certificate.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125
  1. const _ = require('lodash');
  2. const fs = require('fs');
  3. const tempWrite = require('temp-write');
  4. const moment = require('moment');
  5. const logger = require('../logger').ssl;
  6. const error = require('../lib/error');
  7. const utils = require('../lib/utils');
  8. const certificateModel = require('../models/certificate');
  9. const dnsPlugins = require('../global/certbot-dns-plugins');
  10. const internalAuditLog = require('./audit-log');
  11. const internalNginx = require('./nginx');
  12. const internalHost = require('./host');
  13. const letsencryptStaging = process.env.NODE_ENV !== 'production';
  14. const letsencryptConfig = '/etc/letsencrypt.ini';
  15. const certbotCommand = 'certbot';
  16. const archiver = require('archiver');
  17. const path = require('path');
  18. function omissions() {
  19. return ['is_deleted'];
  20. }
  21. const internalCertificate = {
  22. allowedSslFiles: ['certificate', 'certificate_key', 'intermediate_certificate'],
  23. intervalTimeout: 1000 * 60 * 60, // 1 hour
  24. interval: null,
  25. intervalProcessing: false,
  26. initTimer: () => {
  27. logger.info('Let\'s Encrypt Renewal Timer initialized');
  28. internalCertificate.interval = setInterval(internalCertificate.processExpiringHosts, internalCertificate.intervalTimeout);
  29. // And do this now as well
  30. internalCertificate.processExpiringHosts();
  31. },
  32. /**
  33. * Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required
  34. */
  35. processExpiringHosts: () => {
  36. if (!internalCertificate.intervalProcessing) {
  37. internalCertificate.intervalProcessing = true;
  38. logger.info('Renewing SSL certs close to expiry...');
  39. const cmd = certbotCommand + ' renew --non-interactive --quiet ' +
  40. '--config "' + letsencryptConfig + '" ' +
  41. '--preferred-challenges "dns,http" ' +
  42. '--disable-hook-validation ' +
  43. (letsencryptStaging ? '--staging' : '');
  44. return utils.exec(cmd)
  45. .then((result) => {
  46. if (result) {
  47. logger.info('Renew Result: ' + result);
  48. }
  49. return internalNginx.reload()
  50. .then(() => {
  51. logger.info('Renew Complete');
  52. return result;
  53. });
  54. })
  55. .then(() => {
  56. // Now go and fetch all the letsencrypt certs from the db and query the files and update expiry times
  57. return certificateModel
  58. .query()
  59. .where('is_deleted', 0)
  60. .andWhere('provider', 'letsencrypt')
  61. .then((certificates) => {
  62. if (certificates && certificates.length) {
  63. let promises = [];
  64. certificates.map(function (certificate) {
  65. promises.push(
  66. internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem')
  67. .then((cert_info) => {
  68. return certificateModel
  69. .query()
  70. .where('id', certificate.id)
  71. .andWhere('provider', 'letsencrypt')
  72. .patch({
  73. expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
  74. });
  75. })
  76. .catch((err) => {
  77. // Don't want to stop the train here, just log the error
  78. logger.error(err.message);
  79. })
  80. );
  81. });
  82. return Promise.all(promises);
  83. }
  84. });
  85. })
  86. .then(() => {
  87. internalCertificate.intervalProcessing = false;
  88. })
  89. .catch((err) => {
  90. logger.error(err);
  91. internalCertificate.intervalProcessing = false;
  92. });
  93. }
  94. },
  95. /**
  96. * @param {Access} access
  97. * @param {Object} data
  98. * @returns {Promise}
  99. */
  100. create: (access, data) => {
  101. return access.can('certificates:create', data)
  102. .then(() => {
  103. data.owner_user_id = access.token.getUserId(1);
  104. if (data.provider === 'letsencrypt') {
  105. data.nice_name = data.domain_names.sort().join(', ');
  106. }
  107. return certificateModel
  108. .query()
  109. .omit(omissions())
  110. .insertAndFetch(data);
  111. })
  112. .then((certificate) => {
  113. if (certificate.provider === 'letsencrypt') {
  114. // Request a new Cert from LE. Let the fun begin.
  115. // 1. Find out any hosts that are using any of the hostnames in this cert
  116. // 2. Disable them in nginx temporarily
  117. // 3. Generate the LE config
  118. // 4. Request cert
  119. // 5. Remove LE config
  120. // 6. Re-instate previously disabled hosts
  121. // 1. Find out any hosts that are using any of the hostnames in this cert
  122. return internalHost.getHostsWithDomains(certificate.domain_names)
  123. .then((in_use_result) => {
  124. // 2. Disable them in nginx temporarily
  125. return internalCertificate.disableInUseHosts(in_use_result)
  126. .then(() => {
  127. return in_use_result;
  128. });
  129. })
  130. .then((in_use_result) => {
  131. // With DNS challenge no config is needed, so skip 3 and 5.
  132. if (certificate.meta.dns_challenge) {
  133. return internalNginx.reload().then(() => {
  134. // 4. Request cert
  135. return internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate);
  136. })
  137. .then(internalNginx.reload)
  138. .then(() => {
  139. // 6. Re-instate previously disabled hosts
  140. return internalCertificate.enableInUseHosts(in_use_result);
  141. })
  142. .then(() => {
  143. return certificate;
  144. })
  145. .catch((err) => {
  146. // In the event of failure, revert things and throw err back
  147. return internalCertificate.enableInUseHosts(in_use_result)
  148. .then(internalNginx.reload)
  149. .then(() => {
  150. throw err;
  151. });
  152. });
  153. } else {
  154. // 3. Generate the LE config
  155. return internalNginx.generateLetsEncryptRequestConfig(certificate)
  156. .then(internalNginx.reload)
  157. .then(() => {
  158. // 4. Request cert
  159. return internalCertificate.requestLetsEncryptSsl(certificate);
  160. })
  161. .then(() => {
  162. // 5. Remove LE config
  163. return internalNginx.deleteLetsEncryptRequestConfig(certificate);
  164. })
  165. .then(internalNginx.reload)
  166. .then(() => {
  167. // 6. Re-instate previously disabled hosts
  168. return internalCertificate.enableInUseHosts(in_use_result);
  169. })
  170. .then(() => {
  171. return certificate;
  172. })
  173. .catch((err) => {
  174. // In the event of failure, revert things and throw err back
  175. return internalNginx.deleteLetsEncryptRequestConfig(certificate)
  176. .then(() => {
  177. return internalCertificate.enableInUseHosts(in_use_result);
  178. })
  179. .then(internalNginx.reload)
  180. .then(() => {
  181. throw err;
  182. });
  183. });
  184. }
  185. })
  186. .then(() => {
  187. // At this point, the letsencrypt cert should exist on disk.
  188. // Lets get the expiry date from the file and update the row silently
  189. return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem')
  190. .then((cert_info) => {
  191. return certificateModel
  192. .query()
  193. .patchAndFetchById(certificate.id, {
  194. expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
  195. })
  196. .then((saved_row) => {
  197. // Add cert data for audit log
  198. saved_row.meta = _.assign({}, saved_row.meta, {
  199. letsencrypt_certificate: cert_info
  200. });
  201. return saved_row;
  202. });
  203. });
  204. }).catch(async (error) => {
  205. // Delete the certificate from the database if it was not created successfully
  206. await certificateModel
  207. .query()
  208. .deleteById(certificate.id);
  209. throw error;
  210. });
  211. } else {
  212. return certificate;
  213. }
  214. }).then((certificate) => {
  215. data.meta = _.assign({}, data.meta || {}, certificate.meta);
  216. // Add to audit log
  217. return internalAuditLog.add(access, {
  218. action: 'created',
  219. object_type: 'certificate',
  220. object_id: certificate.id,
  221. meta: data
  222. })
  223. .then(() => {
  224. return certificate;
  225. });
  226. });
  227. },
  228. /**
  229. * @param {Access} access
  230. * @param {Object} data
  231. * @param {Number} data.id
  232. * @param {String} [data.email]
  233. * @param {String} [data.name]
  234. * @return {Promise}
  235. */
  236. update: (access, data) => {
  237. return access.can('certificates:update', data.id)
  238. .then((/*access_data*/) => {
  239. return internalCertificate.get(access, {id: data.id});
  240. })
  241. .then((row) => {
  242. if (row.id !== data.id) {
  243. // Sanity check that something crazy hasn't happened
  244. throw new error.InternalValidationError('Certificate could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
  245. }
  246. return certificateModel
  247. .query()
  248. .omit(omissions())
  249. .patchAndFetchById(row.id, data)
  250. .then((saved_row) => {
  251. saved_row.meta = internalCertificate.cleanMeta(saved_row.meta);
  252. data.meta = internalCertificate.cleanMeta(data.meta);
  253. // Add row.nice_name for custom certs
  254. if (saved_row.provider === 'other') {
  255. data.nice_name = saved_row.nice_name;
  256. }
  257. // Add to audit log
  258. return internalAuditLog.add(access, {
  259. action: 'updated',
  260. object_type: 'certificate',
  261. object_id: row.id,
  262. meta: _.omit(data, ['expires_on']) // this prevents json circular reference because expires_on might be raw
  263. })
  264. .then(() => {
  265. return _.omit(saved_row, omissions());
  266. });
  267. });
  268. });
  269. },
  270. /**
  271. * @param {Access} access
  272. * @param {Object} data
  273. * @param {Number} data.id
  274. * @param {Array} [data.expand]
  275. * @param {Array} [data.omit]
  276. * @return {Promise}
  277. */
  278. get: (access, data) => {
  279. if (typeof data === 'undefined') {
  280. data = {};
  281. }
  282. return access.can('certificates:get', data.id)
  283. .then((access_data) => {
  284. let query = certificateModel
  285. .query()
  286. .where('is_deleted', 0)
  287. .andWhere('id', data.id)
  288. .allowEager('[owner]')
  289. .first();
  290. if (access_data.permission_visibility !== 'all') {
  291. query.andWhere('owner_user_id', access.token.getUserId(1));
  292. }
  293. // Custom omissions
  294. if (typeof data.omit !== 'undefined' && data.omit !== null) {
  295. query.omit(data.omit);
  296. }
  297. if (typeof data.expand !== 'undefined' && data.expand !== null) {
  298. query.eager('[' + data.expand.join(', ') + ']');
  299. }
  300. return query;
  301. })
  302. .then((row) => {
  303. if (row) {
  304. return _.omit(row, omissions());
  305. } else {
  306. throw new error.ItemNotFoundError(data.id);
  307. }
  308. });
  309. },
  310. /**
  311. * @param {Access} access
  312. * @param {Object} data
  313. * @param {Number} data.id
  314. * @returns {Promise}
  315. */
  316. download: (access, data) => {
  317. return new Promise((resolve, reject) => {
  318. access.can('certificates:get', data)
  319. .then(() => {
  320. return internalCertificate.get(access, data);
  321. })
  322. .then((certificate) => {
  323. if (certificate.provider === 'letsencrypt') {
  324. const zipDirectory = '/etc/letsencrypt/live/npm-' + data.id;
  325. if (!fs.existsSync(zipDirectory)) {
  326. throw new error.ItemNotFoundError('Certificate ' + certificate.nice_name + ' does not exists');
  327. }
  328. let certFiles = fs.readdirSync(zipDirectory)
  329. .filter((fn) => fn.endsWith('.pem'))
  330. .map((fn) => fs.realpathSync(path.join(zipDirectory, fn)));
  331. const downloadName = 'npm-' + data.id + '-' + `${Date.now()}.zip`;
  332. const opName = '/tmp/' + downloadName;
  333. internalCertificate.zipFiles(certFiles, opName)
  334. .then(() => {
  335. logger.debug('zip completed : ', opName);
  336. const resp = {
  337. fileName: opName
  338. };
  339. resolve(resp);
  340. }).catch((err) => reject(err));
  341. } else {
  342. throw new error.ValidationError('Only Let\'sEncrypt certificates can be downloaded');
  343. }
  344. }).catch((err) => reject(err));
  345. });
  346. },
  347. /**
  348. * @param {String} source
  349. * @param {String} out
  350. * @returns {Promise}
  351. */
  352. zipFiles(source, out) {
  353. const archive = archiver('zip', { zlib: { level: 9 } });
  354. const stream = fs.createWriteStream(out);
  355. return new Promise((resolve, reject) => {
  356. source
  357. .map((fl) => {
  358. let fileName = path.basename(fl);
  359. logger.debug(fl, 'added to certificate zip');
  360. archive.file(fl, { name: fileName });
  361. });
  362. archive
  363. .on('error', (err) => reject(err))
  364. .pipe(stream);
  365. stream.on('close', () => resolve());
  366. archive.finalize();
  367. });
  368. },
  369. /**
  370. * @param {Access} access
  371. * @param {Object} data
  372. * @param {Number} data.id
  373. * @param {String} [data.reason]
  374. * @returns {Promise}
  375. */
  376. delete: (access, data) => {
  377. return access.can('certificates:delete', data.id)
  378. .then(() => {
  379. return internalCertificate.get(access, {id: data.id});
  380. })
  381. .then((row) => {
  382. if (!row) {
  383. throw new error.ItemNotFoundError(data.id);
  384. }
  385. return certificateModel
  386. .query()
  387. .where('id', row.id)
  388. .patch({
  389. is_deleted: 1
  390. })
  391. .then(() => {
  392. // Add to audit log
  393. row.meta = internalCertificate.cleanMeta(row.meta);
  394. return internalAuditLog.add(access, {
  395. action: 'deleted',
  396. object_type: 'certificate',
  397. object_id: row.id,
  398. meta: _.omit(row, omissions())
  399. });
  400. })
  401. .then(() => {
  402. if (row.provider === 'letsencrypt') {
  403. // Revoke the cert
  404. return internalCertificate.revokeLetsEncryptSsl(row);
  405. }
  406. });
  407. })
  408. .then(() => {
  409. return true;
  410. });
  411. },
  412. /**
  413. * All Certs
  414. *
  415. * @param {Access} access
  416. * @param {Array} [expand]
  417. * @param {String} [search_query]
  418. * @returns {Promise}
  419. */
  420. getAll: (access, expand, search_query) => {
  421. return access.can('certificates:list')
  422. .then((access_data) => {
  423. let query = certificateModel
  424. .query()
  425. .where('is_deleted', 0)
  426. .groupBy('id')
  427. .omit(['is_deleted'])
  428. .allowEager('[owner]')
  429. .orderBy('nice_name', 'ASC');
  430. if (access_data.permission_visibility !== 'all') {
  431. query.andWhere('owner_user_id', access.token.getUserId(1));
  432. }
  433. // Query is used for searching
  434. if (typeof search_query === 'string') {
  435. query.where(function () {
  436. this.where('name', 'like', '%' + search_query + '%');
  437. });
  438. }
  439. if (typeof expand !== 'undefined' && expand !== null) {
  440. query.eager('[' + expand.join(', ') + ']');
  441. }
  442. return query;
  443. });
  444. },
  445. /**
  446. * Report use
  447. *
  448. * @param {Number} user_id
  449. * @param {String} visibility
  450. * @returns {Promise}
  451. */
  452. getCount: (user_id, visibility) => {
  453. let query = certificateModel
  454. .query()
  455. .count('id as count')
  456. .where('is_deleted', 0);
  457. if (visibility !== 'all') {
  458. query.andWhere('owner_user_id', user_id);
  459. }
  460. return query.first()
  461. .then((row) => {
  462. return parseInt(row.count, 10);
  463. });
  464. },
  465. /**
  466. * @param {Object} certificate
  467. * @returns {Promise}
  468. */
  469. writeCustomCert: (certificate) => {
  470. logger.info('Writing Custom Certificate:', certificate);
  471. const dir = '/data/custom_ssl/npm-' + certificate.id;
  472. return new Promise((resolve, reject) => {
  473. if (certificate.provider === 'letsencrypt') {
  474. reject(new Error('Refusing to write letsencrypt certs here'));
  475. return;
  476. }
  477. let certData = certificate.meta.certificate;
  478. if (typeof certificate.meta.intermediate_certificate !== 'undefined') {
  479. certData = certData + '\n' + certificate.meta.intermediate_certificate;
  480. }
  481. try {
  482. if (!fs.existsSync(dir)) {
  483. fs.mkdirSync(dir);
  484. }
  485. } catch (err) {
  486. reject(err);
  487. return;
  488. }
  489. fs.writeFile(dir + '/fullchain.pem', certData, function (err) {
  490. if (err) {
  491. reject(err);
  492. } else {
  493. resolve();
  494. }
  495. });
  496. })
  497. .then(() => {
  498. return new Promise((resolve, reject) => {
  499. fs.writeFile(dir + '/privkey.pem', certificate.meta.certificate_key, function (err) {
  500. if (err) {
  501. reject(err);
  502. } else {
  503. resolve();
  504. }
  505. });
  506. });
  507. });
  508. },
  509. /**
  510. * @param {Access} access
  511. * @param {Object} data
  512. * @param {Array} data.domain_names
  513. * @param {String} data.meta.letsencrypt_email
  514. * @param {Boolean} data.meta.letsencrypt_agree
  515. * @returns {Promise}
  516. */
  517. createQuickCertificate: (access, data) => {
  518. return internalCertificate.create(access, {
  519. provider: 'letsencrypt',
  520. domain_names: data.domain_names,
  521. meta: data.meta
  522. });
  523. },
  524. /**
  525. * Validates that the certs provided are good.
  526. * No access required here, nothing is changed or stored.
  527. *
  528. * @param {Object} data
  529. * @param {Object} data.files
  530. * @returns {Promise}
  531. */
  532. validate: (data) => {
  533. return new Promise((resolve) => {
  534. // Put file contents into an object
  535. let files = {};
  536. _.map(data.files, (file, name) => {
  537. if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
  538. files[name] = file.data.toString();
  539. }
  540. });
  541. resolve(files);
  542. })
  543. .then((files) => {
  544. // For each file, create a temp file and write the contents to it
  545. // Then test it depending on the file type
  546. let promises = [];
  547. _.map(files, (content, type) => {
  548. promises.push(new Promise((resolve) => {
  549. if (type === 'certificate_key') {
  550. resolve(internalCertificate.checkPrivateKey(content));
  551. } else {
  552. // this should handle `certificate` and intermediate certificate
  553. resolve(internalCertificate.getCertificateInfo(content, true));
  554. }
  555. }).then((res) => {
  556. return {[type]: res};
  557. }));
  558. });
  559. return Promise.all(promises)
  560. .then((files) => {
  561. let data = {};
  562. _.each(files, (file) => {
  563. data = _.assign({}, data, file);
  564. });
  565. return data;
  566. });
  567. });
  568. },
  569. /**
  570. * @param {Access} access
  571. * @param {Object} data
  572. * @param {Number} data.id
  573. * @param {Object} data.files
  574. * @returns {Promise}
  575. */
  576. upload: (access, data) => {
  577. return internalCertificate.get(access, {id: data.id})
  578. .then((row) => {
  579. if (row.provider !== 'other') {
  580. throw new error.ValidationError('Cannot upload certificates for this type of provider');
  581. }
  582. return internalCertificate.validate(data)
  583. .then((validations) => {
  584. if (typeof validations.certificate === 'undefined') {
  585. throw new error.ValidationError('Certificate file was not provided');
  586. }
  587. _.map(data.files, (file, name) => {
  588. if (internalCertificate.allowedSslFiles.indexOf(name) !== -1) {
  589. row.meta[name] = file.data.toString();
  590. }
  591. });
  592. // TODO: This uses a mysql only raw function that won't translate to postgres
  593. return internalCertificate.update(access, {
  594. id: data.id,
  595. expires_on: moment(validations.certificate.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss'),
  596. domain_names: [validations.certificate.cn],
  597. meta: _.clone(row.meta) // Prevent the update method from changing this value that we'll use later
  598. })
  599. .then((certificate) => {
  600. console.log('ROWMETA:', row.meta);
  601. certificate.meta = row.meta;
  602. return internalCertificate.writeCustomCert(certificate);
  603. });
  604. })
  605. .then(() => {
  606. return _.pick(row.meta, internalCertificate.allowedSslFiles);
  607. });
  608. });
  609. },
  610. /**
  611. * Uses the openssl command to validate the private key.
  612. * It will save the file to disk first, then run commands on it, then delete the file.
  613. *
  614. * @param {String} private_key This is the entire key contents as a string
  615. */
  616. checkPrivateKey: (private_key) => {
  617. return tempWrite(private_key, '/tmp')
  618. .then((filepath) => {
  619. return new Promise((resolve, reject) => {
  620. const failTimeout = setTimeout(() => {
  621. reject(new error.ValidationError('Result Validation Error: Validation timed out. This could be due to the key being passphrase-protected.'));
  622. }, 10000);
  623. utils
  624. .exec('openssl pkey -in ' + filepath + ' -check -noout 2>&1 ')
  625. .then((result) => {
  626. clearTimeout(failTimeout);
  627. if (!result.toLowerCase().includes('key is valid')) {
  628. reject(new error.ValidationError('Result Validation Error: ' + result));
  629. }
  630. fs.unlinkSync(filepath);
  631. resolve(true);
  632. })
  633. .catch((err) => {
  634. clearTimeout(failTimeout);
  635. fs.unlinkSync(filepath);
  636. reject(new error.ValidationError('Certificate Key is not valid (' + err.message + ')', err));
  637. });
  638. });
  639. });
  640. },
  641. /**
  642. * Uses the openssl command to both validate and get info out of the certificate.
  643. * It will save the file to disk first, then run commands on it, then delete the file.
  644. *
  645. * @param {String} certificate This is the entire cert contents as a string
  646. * @param {Boolean} [throw_expired] Throw when the certificate is out of date
  647. */
  648. getCertificateInfo: (certificate, throw_expired) => {
  649. return tempWrite(certificate, '/tmp')
  650. .then((filepath) => {
  651. return internalCertificate.getCertificateInfoFromFile(filepath, throw_expired)
  652. .then((certData) => {
  653. fs.unlinkSync(filepath);
  654. return certData;
  655. }).catch((err) => {
  656. fs.unlinkSync(filepath);
  657. throw err;
  658. });
  659. });
  660. },
  661. /**
  662. * Uses the openssl command to both validate and get info out of the certificate.
  663. * It will save the file to disk first, then run commands on it, then delete the file.
  664. *
  665. * @param {String} certificate_file The file location on disk
  666. * @param {Boolean} [throw_expired] Throw when the certificate is out of date
  667. */
  668. getCertificateInfoFromFile: (certificate_file, throw_expired) => {
  669. let certData = {};
  670. return utils.exec('openssl x509 -in ' + certificate_file + ' -subject -noout')
  671. .then((result) => {
  672. // subject=CN = something.example.com
  673. const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim;
  674. const match = regex.exec(result);
  675. if (typeof match[1] === 'undefined') {
  676. throw new error.ValidationError('Could not determine subject from certificate: ' + result);
  677. }
  678. certData['cn'] = match[1];
  679. })
  680. .then(() => {
  681. return utils.exec('openssl x509 -in ' + certificate_file + ' -issuer -noout');
  682. })
  683. .then((result) => {
  684. // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
  685. const regex = /^(?:issuer=)?(.*)$/gim;
  686. const match = regex.exec(result);
  687. if (typeof match[1] === 'undefined') {
  688. throw new error.ValidationError('Could not determine issuer from certificate: ' + result);
  689. }
  690. certData['issuer'] = match[1];
  691. })
  692. .then(() => {
  693. return utils.exec('openssl x509 -in ' + certificate_file + ' -dates -noout');
  694. })
  695. .then((result) => {
  696. // notBefore=Jul 14 04:04:29 2018 GMT
  697. // notAfter=Oct 12 04:04:29 2018 GMT
  698. let validFrom = null;
  699. let validTo = null;
  700. const lines = result.split('\n');
  701. lines.map(function (str) {
  702. const regex = /^(\S+)=(.*)$/gim;
  703. const match = regex.exec(str.trim());
  704. if (match && typeof match[2] !== 'undefined') {
  705. const date = parseInt(moment(match[2], 'MMM DD HH:mm:ss YYYY z').format('X'), 10);
  706. if (match[1].toLowerCase() === 'notbefore') {
  707. validFrom = date;
  708. } else if (match[1].toLowerCase() === 'notafter') {
  709. validTo = date;
  710. }
  711. }
  712. });
  713. if (!validFrom || !validTo) {
  714. throw new error.ValidationError('Could not determine dates from certificate: ' + result);
  715. }
  716. if (throw_expired && validTo < parseInt(moment().format('X'), 10)) {
  717. throw new error.ValidationError('Certificate has expired');
  718. }
  719. certData['dates'] = {
  720. from: validFrom,
  721. to: validTo
  722. };
  723. return certData;
  724. }).catch((err) => {
  725. throw new error.ValidationError('Certificate is not valid (' + err.message + ')', err);
  726. });
  727. },
  728. /**
  729. * Cleans the ssl keys from the meta object and sets them to "true"
  730. *
  731. * @param {Object} meta
  732. * @param {Boolean} [remove]
  733. * @returns {Object}
  734. */
  735. cleanMeta: function (meta, remove) {
  736. internalCertificate.allowedSslFiles.map((key) => {
  737. if (typeof meta[key] !== 'undefined' && meta[key]) {
  738. if (remove) {
  739. delete meta[key];
  740. } else {
  741. meta[key] = true;
  742. }
  743. }
  744. });
  745. return meta;
  746. },
  747. /**
  748. * Request a certificate using the http challenge
  749. * @param {Object} certificate the certificate row
  750. * @returns {Promise}
  751. */
  752. requestLetsEncryptSsl: (certificate) => {
  753. logger.info('Requesting Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
  754. const cmd = certbotCommand + ' certonly --non-interactive ' +
  755. '--config "' + letsencryptConfig + '" ' +
  756. '--cert-name "npm-' + certificate.id + '" ' +
  757. '--agree-tos ' +
  758. '--authenticator webroot ' +
  759. '--email "' + certificate.meta.letsencrypt_email + '" ' +
  760. '--preferred-challenges "dns,http" ' +
  761. '--domains "' + certificate.domain_names.join(',') + '" ' +
  762. (letsencryptStaging ? '--staging' : '');
  763. logger.info('Command:', cmd);
  764. return utils.exec(cmd)
  765. .then((result) => {
  766. logger.success(result);
  767. return result;
  768. });
  769. },
  770. /**
  771. * @param {Object} certificate the certificate row
  772. * @param {String} dns_provider the dns provider name (key used in `certbot-dns-plugins.js`)
  773. * @param {String | null} credentials the content of this providers credentials file
  774. * @param {String} propagation_seconds the cloudflare api token
  775. * @returns {Promise}
  776. */
  777. requestLetsEncryptSslWithDnsChallenge: (certificate) => {
  778. const dns_plugin = dnsPlugins[certificate.meta.dns_provider];
  779. if (!dns_plugin) {
  780. throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
  781. }
  782. logger.info(`Requesting Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
  783. const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
  784. const credentialsCmd = 'mkdir -p /etc/letsencrypt/credentials 2> /dev/null; echo \'' + certificate.meta.dns_provider_credentials.replace('\'', '\\\'') + '\' > \'' + credentialsLocation + '\' && chmod 600 \'' + credentialsLocation + '\'';
  785. const prepareCmd = 'pip install ' + dns_plugin.package_name + (dns_plugin.version_requirement || '') + ' ' + dns_plugin.dependencies;
  786. // Whether the plugin has a --<name>-credentials argument
  787. const hasConfigArg = certificate.meta.dns_provider !== 'route53';
  788. let mainCmd = certbotCommand + ' certonly --non-interactive ' +
  789. '--cert-name "npm-' + certificate.id + '" ' +
  790. '--agree-tos ' +
  791. '--email "' + certificate.meta.letsencrypt_email + '" ' +
  792. '--domains "' + certificate.domain_names.join(',') + '" ' +
  793. '--authenticator ' + dns_plugin.full_plugin_name + ' ' +
  794. (
  795. hasConfigArg
  796. ? '--' + dns_plugin.full_plugin_name + '-credentials "' + credentialsLocation + '"'
  797. : ''
  798. ) +
  799. (
  800. certificate.meta.propagation_seconds !== undefined
  801. ? ' --' + dns_plugin.full_plugin_name + '-propagation-seconds ' + certificate.meta.propagation_seconds
  802. : ''
  803. ) +
  804. (letsencryptStaging ? ' --staging' : '');
  805. // Prepend the path to the credentials file as an environment variable
  806. if (certificate.meta.dns_provider === 'route53') {
  807. mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
  808. }
  809. logger.info('Command:', `${credentialsCmd} && ${prepareCmd} && ${mainCmd}`);
  810. return utils.exec(credentialsCmd)
  811. .then(() => {
  812. return utils.exec(prepareCmd)
  813. .then(() => {
  814. return utils.exec(mainCmd)
  815. .then(async (result) => {
  816. logger.info(result);
  817. return result;
  818. });
  819. });
  820. }).catch(async (err) => {
  821. // Don't fail if file does not exist
  822. const delete_credentialsCmd = `rm -f '${credentialsLocation}' || true`;
  823. await utils.exec(delete_credentialsCmd);
  824. throw err;
  825. });
  826. },
  827. /**
  828. * @param {Access} access
  829. * @param {Object} data
  830. * @param {Number} data.id
  831. * @returns {Promise}
  832. */
  833. renew: (access, data) => {
  834. return access.can('certificates:update', data)
  835. .then(() => {
  836. return internalCertificate.get(access, data);
  837. })
  838. .then((certificate) => {
  839. if (certificate.provider === 'letsencrypt') {
  840. const renewMethod = certificate.meta.dns_challenge ? internalCertificate.renewLetsEncryptSslWithDnsChallenge : internalCertificate.renewLetsEncryptSsl;
  841. return renewMethod(certificate)
  842. .then(() => {
  843. return internalCertificate.getCertificateInfoFromFile('/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem');
  844. })
  845. .then((cert_info) => {
  846. return certificateModel
  847. .query()
  848. .patchAndFetchById(certificate.id, {
  849. expires_on: moment(cert_info.dates.to, 'X').format('YYYY-MM-DD HH:mm:ss')
  850. });
  851. })
  852. .then((updated_certificate) => {
  853. // Add to audit log
  854. return internalAuditLog.add(access, {
  855. action: 'renewed',
  856. object_type: 'certificate',
  857. object_id: updated_certificate.id,
  858. meta: updated_certificate
  859. })
  860. .then(() => {
  861. return updated_certificate;
  862. });
  863. });
  864. } else {
  865. throw new error.ValidationError('Only Let\'sEncrypt certificates can be renewed');
  866. }
  867. });
  868. },
  869. /**
  870. * @param {Object} certificate the certificate row
  871. * @returns {Promise}
  872. */
  873. renewLetsEncryptSsl: (certificate) => {
  874. logger.info('Renewing Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
  875. const cmd = certbotCommand + ' renew --force-renewal --non-interactive ' +
  876. '--config "' + letsencryptConfig + '" ' +
  877. '--cert-name "npm-' + certificate.id + '" ' +
  878. '--preferred-challenges "dns,http" ' +
  879. '--disable-hook-validation ' +
  880. (letsencryptStaging ? '--staging' : '');
  881. logger.info('Command:', cmd);
  882. return utils.exec(cmd)
  883. .then((result) => {
  884. logger.info(result);
  885. return result;
  886. });
  887. },
  888. /**
  889. * @param {Object} certificate the certificate row
  890. * @returns {Promise}
  891. */
  892. renewLetsEncryptSslWithDnsChallenge: (certificate) => {
  893. const dns_plugin = dnsPlugins[certificate.meta.dns_provider];
  894. if (!dns_plugin) {
  895. throw Error(`Unknown DNS provider '${certificate.meta.dns_provider}'`);
  896. }
  897. logger.info(`Renewing Let'sEncrypt certificates via ${dns_plugin.display_name} for Cert #${certificate.id}: ${certificate.domain_names.join(', ')}`);
  898. let mainCmd = certbotCommand + ' renew --non-interactive ' +
  899. '--cert-name "npm-' + certificate.id + '" ' +
  900. '--disable-hook-validation' +
  901. (letsencryptStaging ? ' --staging' : '');
  902. // Prepend the path to the credentials file as an environment variable
  903. if (certificate.meta.dns_provider === 'route53') {
  904. const credentialsLocation = '/etc/letsencrypt/credentials/credentials-' + certificate.id;
  905. mainCmd = 'AWS_CONFIG_FILE=\'' + credentialsLocation + '\' ' + mainCmd;
  906. }
  907. logger.info('Command:', mainCmd);
  908. return utils.exec(mainCmd)
  909. .then(async (result) => {
  910. logger.info(result);
  911. return result;
  912. });
  913. },
  914. /**
  915. * @param {Object} certificate the certificate row
  916. * @param {Boolean} [throw_errors]
  917. * @returns {Promise}
  918. */
  919. revokeLetsEncryptSsl: (certificate, throw_errors) => {
  920. logger.info('Revoking Let\'sEncrypt certificates for Cert #' + certificate.id + ': ' + certificate.domain_names.join(', '));
  921. const mainCmd = certbotCommand + ' revoke --non-interactive ' +
  922. '--cert-path "/etc/letsencrypt/live/npm-' + certificate.id + '/fullchain.pem" ' +
  923. '--delete-after-revoke ' +
  924. (letsencryptStaging ? '--staging' : '');
  925. // Don't fail command if file does not exist
  926. const delete_credentialsCmd = `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`;
  927. logger.info('Command:', mainCmd + '; ' + delete_credentialsCmd);
  928. return utils.exec(mainCmd)
  929. .then(async (result) => {
  930. await utils.exec(delete_credentialsCmd);
  931. logger.info(result);
  932. return result;
  933. })
  934. .catch((err) => {
  935. logger.error(err.message);
  936. if (throw_errors) {
  937. throw err;
  938. }
  939. });
  940. },
  941. /**
  942. * @param {Object} certificate
  943. * @returns {Boolean}
  944. */
  945. hasLetsEncryptSslCerts: (certificate) => {
  946. const letsencryptPath = '/etc/letsencrypt/live/npm-' + certificate.id;
  947. return fs.existsSync(letsencryptPath + '/fullchain.pem') && fs.existsSync(letsencryptPath + '/privkey.pem');
  948. },
  949. /**
  950. * @param {Object} in_use_result
  951. * @param {Number} in_use_result.total_count
  952. * @param {Array} in_use_result.proxy_hosts
  953. * @param {Array} in_use_result.redirection_hosts
  954. * @param {Array} in_use_result.dead_hosts
  955. */
  956. disableInUseHosts: (in_use_result) => {
  957. if (in_use_result.total_count) {
  958. let promises = [];
  959. if (in_use_result.proxy_hosts.length) {
  960. promises.push(internalNginx.bulkDeleteConfigs('proxy_host', in_use_result.proxy_hosts));
  961. }
  962. if (in_use_result.redirection_hosts.length) {
  963. promises.push(internalNginx.bulkDeleteConfigs('redirection_host', in_use_result.redirection_hosts));
  964. }
  965. if (in_use_result.dead_hosts.length) {
  966. promises.push(internalNginx.bulkDeleteConfigs('dead_host', in_use_result.dead_hosts));
  967. }
  968. return Promise.all(promises);
  969. } else {
  970. return Promise.resolve();
  971. }
  972. },
  973. /**
  974. * @param {Object} in_use_result
  975. * @param {Number} in_use_result.total_count
  976. * @param {Array} in_use_result.proxy_hosts
  977. * @param {Array} in_use_result.redirection_hosts
  978. * @param {Array} in_use_result.dead_hosts
  979. */
  980. enableInUseHosts: (in_use_result) => {
  981. if (in_use_result.total_count) {
  982. let promises = [];
  983. if (in_use_result.proxy_hosts.length) {
  984. promises.push(internalNginx.bulkGenerateConfigs('proxy_host', in_use_result.proxy_hosts));
  985. }
  986. if (in_use_result.redirection_hosts.length) {
  987. promises.push(internalNginx.bulkGenerateConfigs('redirection_host', in_use_result.redirection_hosts));
  988. }
  989. if (in_use_result.dead_hosts.length) {
  990. promises.push(internalNginx.bulkGenerateConfigs('dead_host', in_use_result.dead_hosts));
  991. }
  992. return Promise.all(promises);
  993. } else {
  994. return Promise.resolve();
  995. }
  996. }
  997. };
  998. module.exports = internalCertificate;