UNPKG

13.1 kBJavaScriptView Raw
1/**
2 * Copyright 2017-2018 F5 Networks, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17'use strict';
18
19const assert = require('assert');
20const fs = require('fs');
21const childProcess = require('child_process');
22const q = require('q');
23const cryptoUtil = require('../lib/cryptoUtil');
24const util = require('../lib/util');
25const Logger = require('./logger');
26const REG_EXPS = require('../lib/sharedConstants').REG_EXPS;
27
28let logger = Logger.getLogger({
29 logLevel: 'none',
30 module
31});
32
33/**
34 * This routines are utilities for setting up public/private keys.
35 *
36 * These routines are meant to be used locally on a BIG-IP and operate via tmsh
37 * rather than iControl REST. This is so that we do not need to take in
38 * unencrypted passwords as parameters either on the command line or via
39 * the filesystem.
40 *
41 * Notes:
42 * + Only runs locally on a BIG-IP. Cannot run on a remote BIG-IP.
43 * + Uses tmsh rather than iControl REST so that we do not need to take in a password
44 *
45 * @module
46 */
47module.exports = {
48 /**
49 * Generates and installs a public/private key pair if not already installed
50 *
51 * @param {String} publicKeyDirctory - Directory into which to write the public key
52 * @param {String} publicKeyOutFile - Filename for public key
53 * @param {String} privateKeyFolder - BIG-IP folder into which to install the private key
54 * @param {String} privateKeyName - Name for private key on BIG-IP
55 * @param {Object} [options] - Optional parameters
56 * @param {Boolean} [options.force] - Force generation even if private key exists
57 * @param {Boolean} [options.installPublic] - Install the public key as an iFile (so that it is synced)
58 *
59 * @returns {Promise} A promise which is resolved with the name of the public key if we
60 * installed one, or rejected if an error occurs.
61 */
62 generateAndInstallKeyPair(
63 publicKeyDirectory,
64 publicKeyOutFile,
65 privateKeyFolder,
66 privateKeyName,
67 options
68 ) {
69 const PASSPHRASE_LENGTH = 18;
70 const PRIVATE_KEY_TEMP_FILE = `/tmp/cloudLibsPrivate${Date.now()}.pem`;
71
72 const force = options ? options.force : false;
73 const installPublic = options ? options.installPublic : false;
74
75 let passphrase;
76 let installedPublicKeyPath;
77
78 assert.equal(typeof publicKeyDirectory, 'string', 'publicKeyDirectory must be a string');
79 assert.equal(typeof publicKeyOutFile, 'string', 'publicKeyOutFile must be a string');
80 assert.equal(typeof privateKeyFolder, 'string', 'privateKeyFolder must be a string');
81 assert.equal(typeof privateKeyName, 'string', 'privateKeyName must be a string');
82
83 // Check to see if we have a key pair yet
84 return this.getExistingPrivateKeyName(privateKeyFolder, privateKeyName)
85 .then((existingPrivateKey) => {
86 if (existingPrivateKey && !force) {
87 logger.debug('Private key already exists');
88 if (installPublic) {
89 return q(this.getKeyFilePath('Common', 'ifile', existingPrivateKey));
90 }
91 return q();
92 }
93
94 if (existingPrivateKey) {
95 logger.debug('Private key exists. Regenerating.');
96 } else {
97 logger.debug('No private key found - generating key pair');
98 }
99
100 return createDirectory(publicKeyDirectory)
101 .then(() => {
102 return cryptoUtil.generateRandomBytes(PASSPHRASE_LENGTH, 'base64');
103 })
104 .then((response) => {
105 passphrase = response;
106 return cryptoUtil.generateKeyPair(
107 PRIVATE_KEY_TEMP_FILE,
108 {
109 publicKeyOutFile,
110 passphrase,
111 keyLength: '3072'
112 }
113 );
114 })
115 .then(() => {
116 return installPrivateKey(
117 PRIVATE_KEY_TEMP_FILE,
118 privateKeyFolder,
119 privateKeyName,
120 { passphrase }
121 );
122 })
123 .then(() => {
124 if (installPublic) {
125 return installPublicKey.call(
126 this,
127 publicKeyOutFile,
128 privateKeyName
129 );
130 }
131 return q();
132 })
133 .then((publicKeyPath) => {
134 installedPublicKeyPath = publicKeyPath;
135 const func = function () {
136 return util.runTmshCommand('save sys config');
137 };
138
139 return util.tryUntil(this, util.MEDIUM_RETRY, func);
140 })
141 .then(() => {
142 return q(installedPublicKeyPath);
143 });
144 });
145 },
146
147 /**
148 *
149 * @param {String} folder - BIG-IP folder name.
150 * @param {String} keyType - File type. For example: certificate_key or ifile.
151 * @param {String} name - Name of key.
152 */
153 getKeyFilePath(folder, keyType, name) {
154 const KEY_DIR = `/config/filestore/files_d/${folder}_d/${keyType}_d/`;
155
156 assert.equal(typeof folder, 'string', 'folder must be a string');
157 assert.equal(typeof name, 'string', 'name must be a string');
158
159 return util.runShellCommand(`ls -1t ${KEY_DIR}`)
160 .then((response) => {
161 const KEY_FILE_PREFIX = `:${folder}:${name}`;
162 const files = response.split('\n');
163 const ourKey = files.find((element) => {
164 return element.startsWith(KEY_FILE_PREFIX);
165 });
166 if (ourKey) {
167 return KEY_DIR + ourKey;
168 }
169 return q();
170 });
171 },
172
173 getPrivateKeyFilePath(folder, name) {
174 return this.getKeyFilePath(folder, 'certificate_key', name);
175 },
176
177 /**
178 * Gets the local private key
179 *
180 * @returns {Promise} A promise which is resolved with the key metadata or
181 * rejected if an error occurs
182 */
183 getPrivateKeyMetadata(folder, name) {
184 assert.equal(typeof folder, 'string', 'folder must be a string');
185 assert.equal(typeof name, 'string', 'name must be a string');
186
187 return listPrivateKey('file ssl-key', folder, name)
188 .then((response) => {
189 return util.parseTmshResponse(response.keyData);
190 });
191 },
192
193 getExistingPrivateKeyName(folder, name) {
194 let privateKeyName;
195 return listPrivateKey('crypto key', folder, name)
196 .then((privateKeyResponse) => {
197 if (privateKeyResponse) {
198 privateKeyName = privateKeyResponse.privateKeyName;
199 return this.getPrivateKeyFilePath(folder, privateKeyName);
200 }
201 return false;
202 })
203 .then((response) => {
204 if (response) {
205 return q(privateKeyName);
206 }
207 return q();
208 });
209 },
210
211
212 setLogger(aLogger) {
213 logger = aLogger;
214 },
215
216 setLoggerOptions(loggerOptions) {
217 const loggerOpts = Object.assign({}, loggerOptions);
218 loggerOpts.module = module;
219 logger = Logger.getLogger(loggerOpts);
220 }
221};
222
223function installPrivateKey(privateKeyFile, folder, name, options) {
224 const deferred = q.defer();
225 let installCmd;
226
227 const passphrase = options ? options.passphrase : undefined;
228
229 installCmd = `install sys crypto key /${folder}/${name} from-local-file ${privateKeyFile}`;
230 if (passphrase) {
231 installCmd += ` passphrase ${passphrase}`;
232 }
233
234 ready()
235 .then(() => {
236 return createBigIpFolder(`/${folder}`);
237 })
238 .then(() => {
239 return util.runTmshCommand(installCmd);
240 })
241 .then(() => {
242 fs.unlink(privateKeyFile, (err) => {
243 if (err) {
244 logger.debug('Failed to delete private key:', err);
245 }
246
247 deferred.resolve();
248 });
249 })
250 .catch((err) => {
251 deferred.reject(err);
252 });
253
254 return deferred.promise;
255}
256
257function installPublicKey(publicKeyPath, name) {
258 return ready()
259 .then(() => {
260 return util.runTmshCommand(`create sys file ifile ${name} source-path file://${publicKeyPath}`);
261 })
262 .then(() => {
263 return this.getKeyFilePath('Common', 'ifile', name);
264 })
265 .catch((err) => {
266 return q.reject(err);
267 });
268}
269
270function ready() {
271 const deferred = q.defer();
272
273 childProcess.execFile(`${__dirname}/../scripts/waitForMcp.sh`, (error) => {
274 if (error) {
275 deferred.reject(error);
276 } else {
277 deferred.resolve();
278 }
279 });
280
281 return deferred.promise;
282}
283
284function createDirectory(directory) {
285 const deferred = q.defer();
286
287 fs.access(directory, (fsAccessErr) => {
288 if (fsAccessErr) {
289 fs.mkdir(directory, (mkdirErr) => {
290 if (mkdirErr) {
291 deferred.reject(mkdirErr);
292 } else {
293 deferred.resolve();
294 }
295 });
296 } else {
297 deferred.resolve();
298 }
299 });
300
301 return deferred.promise;
302}
303
304function createBigIpFolder(folder) {
305 return folderExists(folder)
306 .then((exists) => {
307 if (exists) {
308 return q();
309 }
310 return util.runTmshCommand(`create sys folder ${folder} device-group none traffic-group none`);
311 });
312}
313
314function folderExists(folder) {
315 // tmsh returns an error if trying to list a non-existent folder
316 return util.runTmshCommand(`list sys folder ${folder}`)
317 .then(() => {
318 return q(true);
319 })
320 .catch(() => {
321 return q(false);
322 });
323}
324
325/**
326 * List a private key by folder and name.
327 *
328 * Will first search for a private key with a '.key' suffix, and then without a '.key' suffix
329 *
330 * @param {String} keyType - Key Type to list. Examples: 'file ssl-key', crypto key
331 * @param {String} folder - Folder in which to search for the private key.
332 * @param {String} name - Name of the private key to search for
333 * @param {Boolean} [noRetry] - Whether or not to retry command with a different private key name.
334 *
335 * @returns {Promise} A promise which will be resolved with a dictionary containing the name of the
336 * found private key, and the tmsh command response:
337 *
338 * {
339 * privateKeyName: <String>,
340 * response: <String>
341 * }
342 */
343function listPrivateKey(keyType, folder, name, noRetry) {
344 let privateKeyName;
345
346 // Try with .key suffix first. If unsuccessful, retry without the .key suffix
347 if (noRetry) {
348 // If present, remove '.key' suffix
349 privateKeyName = name.replace(REG_EXPS.KEY_SUFFIX, '');
350 } else {
351 // Append '.key' suffix, if not present
352 privateKeyName = (name.match(REG_EXPS.KEY_SUFFIX)) ? name : `${name}.key`;
353 }
354
355 return util.runTmshCommand(`list sys ${keyType} /${folder}/${privateKeyName}`)
356 .then((keyData) => {
357 // If no result, retry if a retry hasn't occurred yet.
358 if (!keyData) {
359 if (!noRetry) {
360 return listPrivateKey(keyType, folder, name, true);
361 }
362 return q();
363 }
364 return q({ privateKeyName, keyData });
365 })
366 .catch((err) => {
367 // If the object is not found (code: 01020036:3), retry if a retry hasn't occurred yet.
368 const notFoundRegex = /01020036:3/;
369 if (err.message.match(notFoundRegex) && !noRetry) {
370 return listPrivateKey(keyType, folder, name, true);
371 }
372 return q.reject(err);
373 });
374}