From d7d3ceb369bd49def76c3f61c403444f489ad038 Mon Sep 17 00:00:00 2001 From: Joshua Cooper Date: Thu, 25 Nov 2021 15:26:16 -0500 Subject: [PATCH] TLS keys and `https` handling - Added demo key-pairs and cert to `localhost` dir - Added ability to load TLS certificates from disk - Updated web requests to use the `https` module --- localhost/csr.pem | 16 +++++++++ localhost/tls.crt | 19 ++++++++++ localhost/tls.key | 27 ++++++++++++++ munkey/munkey.ts | 29 +++++++++------ munkey/services.ts | 88 ++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 localhost/csr.pem create mode 100644 localhost/tls.crt create mode 100644 localhost/tls.key diff --git a/localhost/csr.pem b/localhost/csr.pem new file mode 100644 index 0000000..9e80d7b --- /dev/null +++ b/localhost/csr.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIChzCCAW8CAQAwQjELMAkGA1UEBhMCVVMxFTATBgNVBAcMDERlZmF1bHQgQ2l0 +eTEcMBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMxePK+2UJJIu37foPBWSOfWy2BVfrqIkEMJ8rCBA0tC +D2GLu7Pp23NN/N1aSfFWKSLBlE9ipxi6x13dTEqj2GO4sq9ONAU87F6C1xuNz3qU +VOrmKQFFfjn2Wi0uULfaHqDUH0JNe1sZBUhXKXAXA3pHznvYjqixxL0QHlEO+knG +yB3fxtPDsVAz1pSCo48HA4gF5HyYypgbqtZvKCW0uLYcNHrigM/K6yhffYrssFGp +B2Tu+CK8Kem6iGgaQVBM/Y4+Dph/83YbaYBGQXyEfGLfOaNZ7EalGU15eCThox1w +mGDKxo31mihQHVVQkRwEOu7ZdAnrnIIqhg1MfbfZGXsCAwEAAaAAMA0GCSqGSIb3 +DQEBCwUAA4IBAQBhQ55pSEvnyIfb5MbjvDqSSwn1+rNZwZc8OQ4nwA4S8NKmKSFT +qtLPa+s5C2ML6cdz0nWOuVgRrPmGTC6T3Wb8vQcrwIBXVwtBJoLz9qK7xg7hJGME +3bSHNv5vZgki6JNM+qyR/UAWMaoe1WvXOtdmm0rH16ye4sSKaKFrHdsyFD5KxlpE +qTCu4/bSSrzPuK6LGDQn1eFiJmS+uOQHfdkVORvkHp535E6a9Ybrsr6WiTByv5bc +zJi4KbV6QtYYjASQb79qF5K02+8Lci7bb02XoxTObZWBSR1iDfwjv8F4BE/+CeNW +RpWPKG6kIwApVUS9IlrlwFiwduCubFM+tceY +-----END CERTIFICATE REQUEST----- diff --git a/localhost/tls.crt b/localhost/tls.crt new file mode 100644 index 0000000..5a89ac4 --- /dev/null +++ b/localhost/tls.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfMCFH6KzlAt2udwaL20C0EwIpsZILf3MA0GCSqGSIb3DQEBCwUAMEIx +CzAJBgNVBAYTAlVTMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl +ZmF1bHQgQ29tcGFueSBMdGQwHhcNMjExMTI1MTkyMjIwWhcNNDkwNDExMTkyMjIw +WjBCMQswCQYDVQQGEwJVUzEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQK +DBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzF48r7ZQkki7ft+g8FZI59bLYFV+uoiQQwnysIEDS0IPYYu7s+nbc038 +3VpJ8VYpIsGUT2KnGLrHXd1MSqPYY7iyr040BTzsXoLXG43PepRU6uYpAUV+OfZa +LS5Qt9oeoNQfQk17WxkFSFcpcBcDekfOe9iOqLHEvRAeUQ76ScbIHd/G08OxUDPW +lIKjjwcDiAXkfJjKmBuq1m8oJbS4thw0euKAz8rrKF99iuywUakHZO74Irwp6bqI +aBpBUEz9jj4OmH/zdhtpgEZBfIR8Yt85o1nsRqUZTXl4JOGjHXCYYMrGjfWaKFAd +VVCRHAQ67tl0CeucgiqGDUx9t9kZewIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA/ +5sHm2veq1OXtC1LrsEr8fwh9k6Xkr536HCdxOlj19dlYeIXfScBbY6mv8v7zuSks +A83hQ9WgG8SvXCDhZiVapkKk+yZ88QrCvhMLyJse0r1NwPNLAVuZqnwQnSICKSU4 +fd7t8isUm3gnI+n/bDlYij3xeTAKAJb+LBuryrUWVXdW1CsmKJmUDRlD90EvtQrQ +8o5q21QGVnRY46x9/rttHAKocxQk3U7lnqYB7iOLT025m80lpO4q67JUdw43XM/b +89sZGgXS0HImqlJSCSHS0n+Cm/GC0B2zPBPUafY3qabSpgYZOKgCofbhA+ArKHn+ +LQxNM1QlUq/qzpSotE5D +-----END CERTIFICATE----- diff --git a/localhost/tls.key b/localhost/tls.key new file mode 100644 index 0000000..86136c0 --- /dev/null +++ b/localhost/tls.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpgIBAAKCAQEAzF48r7ZQkki7ft+g8FZI59bLYFV+uoiQQwnysIEDS0IPYYu7 +s+nbc0383VpJ8VYpIsGUT2KnGLrHXd1MSqPYY7iyr040BTzsXoLXG43PepRU6uYp +AUV+OfZaLS5Qt9oeoNQfQk17WxkFSFcpcBcDekfOe9iOqLHEvRAeUQ76ScbIHd/G +08OxUDPWlIKjjwcDiAXkfJjKmBuq1m8oJbS4thw0euKAz8rrKF99iuywUakHZO74 +Irwp6bqIaBpBUEz9jj4OmH/zdhtpgEZBfIR8Yt85o1nsRqUZTXl4JOGjHXCYYMrG +jfWaKFAdVVCRHAQ67tl0CeucgiqGDUx9t9kZewIDAQABAoIBAQCDnq22/NQnYnBe +5efg4bFSnyOch3N27zz58A49XtmgPotpZ3UcCiErwa55YQz+QV984u+BsSes5Z5A +9aWM7LkQgIOUI+mc9f/FXr7rIAngCGgoYNNH3lnNOrwZHRsfTXssWXFIYl5v7U1Z +qckmR6wVtOlnGbHHM7ZhjV/5FIxdtmG6MibxtkQSgm4T8954XMcziA86LnQ1v5M7 +7mHHyBm7Qe3ib0OdSqVz9O1blFd8BFEG9L9XaHkDhFXO5iCN2XBwl97tpcWQTFHA +mf6dmyjtfBziV0SFErbUyJYDrNRx5UQeCVNOpfT5rpM9eA49xx7QxyVBkyqzLp/a +pMvI5vkBAoGBAOmw4RBrFHYDgkPD7FHYp0avEGmV0JkR+vA+7W01BZr9Igmy6hxp +3EmCNG/L2ZI55Ept/RYBOkDg+bi9bz3WvPYa0MeDOi6XEMsaPUdnWqWWA5icHl4v +/KheUt/iTCibGYWX5Eu7PFdpDdmFzF9A1uTOHuamOZpM1sMOTwLLqHSFAoGBAN/g +vnaH8weejwXFtN+D8LkPmUW1TdTrUiqbYPGJwiCdDhpB37TeLeMQj8bOAk98VKvR +EB9jcw9j/WgaGSkzVvpi4Q2ZnGi9jNaa8wzEErQBnm3Mfw41LsYXt+ZnDg3UFrJ/ +dEBzqYSLao51+VoX/ZyhXzKyvAbT77EVedm/b7X/AoGBANGllyONjNuapkB5Agcj +IF4vK8AtYOgR01e4fHPef1reAK1GzvQSnEduAfDRpiyitwV2yvf0vff6XM25VJTb +ksYOpIJ4XbfyWmR688KdHBs1C6DbXfsNfdLmW97yO3SqQCkzbOHr5WRdoMkmWYSS +vLajm+E7+q1MhdaTfZp6bnOpAoGBAMsdeU/K6giYp4QCKqa7avRLnbCr3FB3q5Vy +YRLi/BhgxYG3EEJlbVZcGUWydFAvKha0V59St/pXqnn/a6KArMIAYdTX8BrrFlNC +Q47qeVmNOnK9nOyD/crFjBhimVKcgHczwYIULdFON7/GcxN1PqgTlG5H0OWU9RtB +s8qFr9F7AoGBAJC/rw2A58NQjv4lQEKHRxP1wkTH5kc7WqJS3kQ4HDNwsAe9YbN8 +SyagwqueZIEEimgXguCYIMnk9Z/gN4tdS2J8UZxDki2VUs2Q0b/M7TRb8MtImdox +SrrNE80JiGaJN7Ys9rlys4kNDD9oXq6Svj4DR+201ykQii7NcdtaABq6 +-----END RSA PRIVATE KEY----- diff --git a/munkey/munkey.ts b/munkey/munkey.ts index f7d8bb2..71bfca3 100644 --- a/munkey/munkey.ts +++ b/munkey/munkey.ts @@ -145,20 +145,27 @@ async function main(services: ServiceContainer): Promise { .then(() => process.exit(0)); } -generateNewIdentity() - .then(id => { - const args = parseCommandLineArgs(process.argv.slice(2)); - const { - root_dir: rootPath, - port: portNum, - discovery_port: discoveryPortNum, - } = args; +const commandLineArgs = parseCommandLineArgs(process.argv.slice(2)); + +generateNewIdentity(commandLineArgs.root_dir) + .then(({ uniqueId, ...keyPair }) => { + // IMPORTANT: This line is to allow for self-signed certificates. + // Since we use TLS only for establishing an encrypted connection, not for validation, + // there is no need to validate the source. So, we set strict TLS to false. + process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; const storedProcedures = { putAttachment: PouchDB.prototype.putAttachment, getAttachment: PouchDB.prototype.getAttachment, }; + const { + root_dir: rootPath, + port: portNum, + discovery_port: discoveryPortNum, + in_memory: isInMemory, + } = commandLineArgs; + // Plugin options are created separately so that we can do full type-checking (see call to .plugin) const pluginOptions: DatabasePluginAttachment = { putEncryptedAttachment(...args) { @@ -252,20 +259,20 @@ generateNewIdentity() const LocalDB = configurePlugins( { prefix: rootPath + path.sep + "munkey" + path.sep, - db: args.in_memory ? MemDown : undefined, + db: isInMemory ? MemDown : undefined, } as PouchDB.Configuration.DatabaseConfiguration, pluginOptions, ); const AdminDB = configurePlugins( { prefix: rootPath + path.sep + "admin" + path.sep, - db: args.in_memory ? MemDown : undefined, + db: isInMemory ? MemDown : undefined, } as PouchDB.Configuration.DatabaseConfiguration, ); return Promise.resolve(configureLogging({ vault: new VaultService(LocalDB), - identity: new IdentityService(id), + identity: new IdentityService(uniqueId, keyPair), activity: new ActivityService(bonjour()), connection: new ConnectionService(), web: new WebService(express()), diff --git a/munkey/services.ts b/munkey/services.ts index a003469..ab9015f 100644 --- a/munkey/services.ts +++ b/munkey/services.ts @@ -14,11 +14,13 @@ import { import express from "express"; import ip from "ip"; +import { readFile } from "fs"; import * as bonjour from "bonjour"; import PouchDB from "pouchdb"; import usePouchDB from "express-pouchdb"; import {randomUUID} from "crypto"; import http from "http"; +import https from "https"; import winston from "winston"; import ErrnoException = NodeJS.ErrnoException; import path from "path"; @@ -64,8 +66,17 @@ type VaultSyncToken = PouchDB.Replication.Sync; * * @returns Promise which resolves to a new unique identifier string. */ -function generateNewIdentity(): Promise { - return Promise.resolve(randomUUID()); +async function generateNewIdentity(rootDir: string): Promise<{ uniqueId: string } & TlsKeyPair> { + const keyPair = await IdentityService.loadTlsKeyPair(rootDir) + .catch(err => { + console.error("Could not load TLS certificate:", err); + return { key: undefined, cert: undefined }; + }); + + return { + uniqueId: randomUUID(), + ...keyPair, + }; } /** @@ -111,7 +122,7 @@ function configureRoutes(services: ServiceContainer, options?: ServerOptions): P return services .admin.initialize() .then(adminService => services.vault.useAdminService(adminService)) - .then(() => services.web.listen(portNum)) + .then(() => services.web.listen({ portNum, tlsKeyPair: services.identity.getTlsKeyPair() })) .then(async () => { if (discoveryPortNum && await services.activity.broadcast( services.identity.getId(), discoveryPortNum, portNum)) @@ -357,6 +368,11 @@ class VaultService extends Service { } } +interface TlsKeyPair { + key: Buffer, + cert: Buffer, +} + /** * @name IdentityService * @summary Service container for local identity information. @@ -368,7 +384,7 @@ class VaultService extends Service { */ class IdentityService extends Service { private readonly uniqueId: string; - constructor(uniqueId: string) { + constructor(uniqueId: string, private readonly keyPair?: TlsKeyPair) { super(); this.uniqueId = uniqueId; } @@ -382,6 +398,32 @@ class IdentityService extends Service { public getId(): string { return this.uniqueId; } + + public static loadTlsKeyPair(rootDir: string, + keyPath: string = path.join(rootDir, "tls.key"), + certPath: string = path.join(rootDir, "tls.crt")): Promise + { + return Promise.all([ + IdentityService.loadKey(keyPath), + IdentityService.loadKey(certPath) + ]) + .then(([ key, cert ]) => ({ key, cert })); + } + + private static loadKey(keyPath: string): Promise { + return new Promise(function(resolve, reject) { + readFile(keyPath, (err, data: Buffer) => { + if (err) reject(err); + else { + resolve(data); + } + }); + }); + } + + public getTlsKeyPair(): TlsKeyPair { + return this.keyPair; + } } /** @@ -430,10 +472,11 @@ class ActivityService extends Service { { const logger = this.logger; const peerResponse: string|null = await new Promise(function(resolve, reject) { - http.get({ + https.get({ hostname, port: portNum?.toString(), path: "/link", + rejectUnauthorized: false, }, function(res: http.IncomingMessage) { const data: string[] = []; @@ -714,7 +757,7 @@ class ConnectionService extends Service { { let connectionMap = this.getOrCreateMap(vaultId); let connectionKey = `${device.hostname}:${device.portNum}`; - let connectionUrl = `http://${connectionKey}/db/${vaultName}` + let connectionUrl = `https://${connectionKey}/db/${vaultName}` if (!connectionMap.get(connectionKey)) { this.logger.info("Adding remote connection to %s", connectionKey); @@ -781,27 +824,52 @@ class ConnectionService extends Service { } } +interface WebServiceListenerOptions { + hostname?: string; + portNum?: number; + tlsKeyPair?: TlsKeyPair; +} + class WebService extends Service { - private server: http.Server; + private server: http.Server | https.Server; private defaultPort: number; + private defaultTlsKeyPair: TlsKeyPair; constructor(private app: express.Application) { super(); this.server = null; this.defaultPort = 8000; + this.defaultTlsKeyPair = null; } public getApplication(): express.Application { return this.app; } - public listen(portNum: number = this.defaultPort, hostname: string = ip.address()): Promise { + public listen(options?: WebServiceListenerOptions): Promise + { + const { + hostname = ip.address(), + portNum = this.defaultPort, + tlsKeyPair = this.defaultTlsKeyPair, + } = options; + this.defaultTlsKeyPair = this.defaultTlsKeyPair ?? tlsKeyPair; + + if (tlsKeyPair) { + this.logger.info("Creating HTTPS server at https://%s:%d", hostname, portNum); + this.server = https.createServer({ rejectUnauthorized: false, ...tlsKeyPair }, this.getApplication()); + } + else { + this.logger.info("Creating HTTP server at http://%s:%d", hostname, portNum); + this.server = http.createServer(this.getApplication()); + } + return new Promise((resolve, reject) => { - const server: http.Server = this.getApplication().listen( + this.server.listen( this.defaultPort = portNum, hostname, () => { this.logger.info("Listening on port %d", portNum); - resolve(server); + resolve(this.server); }) .on("error", (err: ErrnoException) => { if (err.code === "EADDRINUSE") {