@@ -5,16 +5,18 @@ const _ = require('lodash');
55const error = require ( '../lib/error' ) ;
66const certificateModel = require ( '../models/certificate' ) ;
77const internalAuditLog = require ( './audit-log' ) ;
8- const internalHost = require ( './host' ) ;
98const tempWrite = require ( 'temp-write' ) ;
109const utils = require ( '../lib/utils' ) ;
10+ const moment = require ( 'moment' ) ;
1111
1212function omissions ( ) {
1313 return [ 'is_deleted' ] ;
1414}
1515
1616const internalCertificate = {
1717
18+ allowed_ssl_files : [ 'certificate' , 'certificate_key' , 'intermediate_certificate' ] ,
19+
1820 /**
1921 * @param {Access } access
2022 * @param {Object } data
@@ -57,8 +59,39 @@ const internalCertificate = {
5759 update : ( access , data ) => {
5860 return access . can ( 'certificates:update' , data . id )
5961 . then ( access_data => {
60- // TODO
61- return { } ;
62+ return internalCertificate . get ( access , { id : data . id } ) ;
63+ } )
64+ . then ( row => {
65+ if ( row . id !== data . id ) {
66+ // Sanity check that something crazy hasn't happened
67+ throw new error . InternalValidationError ( 'Certificate could not be updated, IDs do not match: ' + row . id + ' !== ' + data . id ) ;
68+ }
69+
70+ return certificateModel
71+ . query ( )
72+ . omit ( omissions ( ) )
73+ . patchAndFetchById ( row . id , data )
74+ . debug ( )
75+ . then ( saved_row => {
76+ saved_row . meta = internalCertificate . cleanMeta ( saved_row . meta ) ;
77+ data . meta = internalCertificate . cleanMeta ( data . meta ) ;
78+
79+ // Add row.nice_name for custom certs
80+ if ( saved_row . provider === 'other' ) {
81+ data . nice_name = saved_row . nice_name ;
82+ }
83+
84+ // Add to audit log
85+ return internalAuditLog . add ( access , {
86+ action : 'updated' ,
87+ object_type : 'certificate' ,
88+ object_id : row . id ,
89+ meta : _ . omit ( data , [ 'expires_on' ] ) // this prevents json circular reference because expires_on might be raw
90+ } )
91+ . then ( ( ) => {
92+ return _ . omit ( saved_row , omissions ( ) ) ;
93+ } ) ;
94+ } ) ;
6295 } ) ;
6396 } ,
6497
@@ -113,10 +146,10 @@ const internalCertificate = {
113146 } ,
114147
115148 /**
116- * @param {Access } access
117- * @param {Object } data
118- * @param {Integer } data.id
119- * @param {String } [data.reason]
149+ * @param {Access } access
150+ * @param {Object } data
151+ * @param {Integer } data.id
152+ * @param {String } [data.reason]
120153 * @returns {Promise }
121154 */
122155 delete : ( access , data ) => {
@@ -134,6 +167,17 @@ const internalCertificate = {
134167 . where ( 'id' , row . id )
135168 . patch ( {
136169 is_deleted : 1
170+ } )
171+ . then ( ( ) => {
172+ // Add to audit log
173+ row . meta = internalCertificate . cleanMeta ( row . meta ) ;
174+
175+ return internalAuditLog . add ( access , {
176+ action : 'deleted' ,
177+ object_type : 'certificate' ,
178+ object_id : row . id ,
179+ meta : _ . omit ( row , omissions ( ) )
180+ } ) ;
137181 } ) ;
138182 } )
139183 . then ( ( ) => {
@@ -204,19 +248,18 @@ const internalCertificate = {
204248
205249 /**
206250 * Validates that the certs provided are good.
207- * This is probably a horrible way to do this .
251+ * No access required here, nothing is changed or stored .
208252 *
209- * @param {Access } access
210253 * @param {Object } data
211254 * @param {Object } data.files
212255 * @returns {Promise }
213256 */
214- validate : ( access , data ) => {
257+ validate : data => {
215258 return new Promise ( resolve => {
216259 // Put file contents into an object
217260 let files = { } ;
218261 _ . map ( data . files , ( file , name ) => {
219- if ( internalHost . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
262+ if ( internalCertificate . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
220263 files [ name ] = file . data . toString ( ) ;
221264 }
222265 } ) ;
@@ -228,56 +271,26 @@ const internalCertificate = {
228271 // Then test it depending on the file type
229272 let promises = [ ] ;
230273 _ . map ( files , ( content , type ) => {
231- promises . push ( tempWrite ( content , '/tmp' )
232- . then ( filepath => {
233- if ( type === 'certificate_key' ) {
234- return utils . exec ( 'openssl rsa -in ' + filepath + ' -check' )
235- . then ( result => {
236- return { tmp : filepath , result : result . split ( "\n" ) . shift ( ) } ;
237- } ) . catch ( err => {
238- return { tmp : filepath , result : false , err : new error . ValidationError ( 'Certificate Key is not valid' ) } ;
239- } ) ;
240-
241- } else if ( type === 'certificate' ) {
242- return utils . exec ( 'openssl x509 -in ' + filepath + ' -text -noout' )
243- . then ( result => {
244- return { tmp : filepath , result : result } ;
245- } ) . catch ( err => {
246- return { tmp : filepath , result : false , err : new error . ValidationError ( 'Certificate is not valid' ) } ;
247- } ) ;
248- } else {
249- return { tmp : filepath , result : false } ;
250- }
251- } )
252- . then ( file_result => {
253- // Remove temp files
254- fs . unlinkSync ( file_result . tmp ) ;
255- delete file_result . tmp ;
256-
257- return { [ type ] : file_result } ;
258- } )
259- ) ;
274+ promises . push ( new Promise ( ( resolve , reject ) => {
275+ if ( type === 'certificate_key' ) {
276+ resolve ( internalCertificate . checkPrivateKey ( content ) ) ;
277+ } else {
278+ // this should handle `certificate` and intermediate certificate
279+ resolve ( internalCertificate . getCertificateInfo ( content , true ) ) ;
280+ }
281+ } ) . then ( res => {
282+ return { [ type ] : res } ;
283+ } ) ) ;
260284 } ) ;
261285
262- // With the results, delete the temp files for security mainly.
263- // If there was an error with any of them, wait until we've done the deleting
264- // before throwing it.
265286 return Promise . all ( promises )
266287 . then ( files => {
267288 let data = { } ;
268- let err = null ;
269289
270290 _ . each ( files , file => {
271291 data = _ . assign ( { } , data , file ) ;
272- if ( typeof file . err !== 'undefined' && file . err ) {
273- err = file . err ;
274- }
275292 } ) ;
276293
277- if ( err ) {
278- throw err ;
279- }
280-
281294 return data ;
282295 } ) ;
283296 } ) ;
@@ -297,28 +310,159 @@ const internalCertificate = {
297310 throw new error . ValidationError ( 'Cannot upload certificates for this type of provider' ) ;
298311 }
299312
300- _ . map ( data . files , ( file , name ) => {
301- if ( internalHost . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
302- row . meta [ name ] = file . data . toString ( ) ;
303- }
304- } ) ;
313+ return internalCertificate . validate ( data )
314+ . then ( validations => {
315+ if ( typeof validations . certificate === 'undefined' ) {
316+ throw new error . ValidationError ( 'Certificate file was not provided' ) ;
317+ }
305318
306- return internalCertificate . update ( access , {
307- id : data . id ,
308- meta : row . meta
309- } ) ;
310- } )
311- . then ( row => {
312- return internalAuditLog . add ( access , {
313- action : 'updated' ,
314- object_type : 'certificate' ,
315- object_id : row . id ,
316- meta : data
317- } )
319+ _ . map ( data . files , ( file , name ) => {
320+ if ( internalCertificate . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
321+ row . meta [ name ] = file . data . toString ( ) ;
322+ }
323+ } ) ;
324+
325+ return internalCertificate . update ( access , {
326+ id : data . id ,
327+ expires_on : certificateModel . raw ( 'FROM_UNIXTIME(' + validations . certificate . dates . to + ')' ) ,
328+ domain_names : [ validations . certificate . cn ] ,
329+ meta : row . meta
330+ } ) ;
331+ } )
318332 . then ( ( ) => {
319- return _ . pick ( row . meta , internalHost . allowed_ssl_files ) ;
333+ return _ . pick ( row . meta , internalCertificate . allowed_ssl_files ) ;
334+ } ) ;
335+ } ) ;
336+ } ,
337+
338+ /**
339+ * Uses the openssl command to validate the private key.
340+ * It will save the file to disk first, then run commands on it, then delete the file.
341+ *
342+ * @param {String } private_key This is the entire key contents as a string
343+ */
344+ checkPrivateKey : private_key => {
345+ return tempWrite ( private_key , '/tmp' )
346+ . then ( filepath => {
347+ return utils . exec ( 'openssl rsa -in ' + filepath + ' -check -noout' )
348+ . then ( result => {
349+ if ( ! result . toLowerCase ( ) . includes ( 'key ok' ) ) {
350+ throw new error . ValidationError ( result ) ;
351+ }
352+
353+ fs . unlinkSync ( filepath ) ;
354+ return true ;
355+ } ) . catch ( err => {
356+ fs . unlinkSync ( filepath ) ;
357+ throw new error . ValidationError ( 'Certificate Key is not valid (' + err . message + ')' , err ) ;
320358 } ) ;
321359 } ) ;
360+ } ,
361+
362+ /**
363+ * Uses the openssl command to both validate and get info out of the certificate.
364+ * It will save the file to disk first, then run commands on it, then delete the file.
365+ *
366+ * @param {String } certificate This is the entire cert contents as a string
367+ * @param {Boolean } [throw_expired] Throw when the certificate is out of date
368+ */
369+ getCertificateInfo : ( certificate , throw_expired ) => {
370+ return tempWrite ( certificate , '/tmp' )
371+ . then ( filepath => {
372+ let cert_data = { } ;
373+
374+ return utils . exec ( 'openssl x509 -in ' + filepath + ' -subject -noout' )
375+ . then ( result => {
376+ // subject=CN = something.example.com
377+ let regex = / (?: s u b j e c t = ) ? [ ^ = ] + = \s + ( \S + ) / gim;
378+ let match = regex . exec ( result ) ;
379+
380+ if ( typeof match [ 1 ] === 'undefined' ) {
381+ throw new error . ValidationError ( 'Could not determine subject from certificate: ' + result ) ;
382+ }
383+
384+ cert_data [ 'cn' ] = match [ 1 ] ;
385+ } )
386+ . then ( ( ) => {
387+ return utils . exec ( 'openssl x509 -in ' + filepath + ' -issuer -noout' ) ;
388+ } )
389+ . then ( result => {
390+ // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
391+ let regex = / ^ (?: i s s u e r = ) ? ( .* ) $ / gim;
392+ let match = regex . exec ( result ) ;
393+
394+ if ( typeof match [ 1 ] === 'undefined' ) {
395+ throw new error . ValidationError ( 'Could not determine issuer from certificate: ' + result ) ;
396+ }
397+
398+ cert_data [ 'issuer' ] = match [ 1 ] ;
399+ } )
400+ . then ( ( ) => {
401+ return utils . exec ( 'openssl x509 -in ' + filepath + ' -dates -noout' ) ;
402+ } )
403+ . then ( result => {
404+ // notBefore=Jul 14 04:04:29 2018 GMT
405+ // notAfter=Oct 12 04:04:29 2018 GMT
406+ let valid_from = null ;
407+ let valid_to = null ;
408+
409+ let lines = result . split ( '\n' ) ;
410+ lines . map ( function ( str ) {
411+ let regex = / ^ ( \S + ) = ( .* ) $ / gim;
412+ let match = regex . exec ( str . trim ( ) ) ;
413+
414+ if ( match && typeof match [ 2 ] !== 'undefined' ) {
415+ let date = parseInt ( moment ( match [ 2 ] , 'MMM DD HH:mm:ss YYYY z' ) . format ( 'X' ) , 10 ) ;
416+
417+ if ( match [ 1 ] . toLowerCase ( ) === 'notbefore' ) {
418+ valid_from = date ;
419+ } else if ( match [ 1 ] . toLowerCase ( ) === 'notafter' ) {
420+ valid_to = date ;
421+ }
422+ }
423+ } ) ;
424+
425+ if ( ! valid_from || ! valid_to ) {
426+ throw new error . ValidationError ( 'Could not determine dates from certificate: ' + result ) ;
427+ }
428+
429+ if ( throw_expired && valid_to < parseInt ( moment ( ) . format ( 'X' ) , 10 ) ) {
430+ throw new error . ValidationError ( 'Certificate has expired' ) ;
431+ }
432+
433+ cert_data [ 'dates' ] = {
434+ from : valid_from ,
435+ to : valid_to
436+ } ;
437+ } )
438+ . then ( ( ) => {
439+ fs . unlinkSync ( filepath ) ;
440+ return cert_data ;
441+ } ) . catch ( err => {
442+ fs . unlinkSync ( filepath ) ;
443+ throw new error . ValidationError ( 'Certificate is not valid (' + err . message + ')' , err ) ;
444+ } ) ;
445+ } ) ;
446+ } ,
447+
448+ /**
449+ * Cleans the ssl keys from the meta object and sets them to "true"
450+ *
451+ * @param {Object } meta
452+ * @param {Boolean } [remove]
453+ * @returns {Object }
454+ */
455+ cleanMeta : function ( meta , remove ) {
456+ internalCertificate . allowed_ssl_files . map ( key => {
457+ if ( typeof meta [ key ] !== 'undefined' && meta [ key ] ) {
458+ if ( remove ) {
459+ delete meta [ key ] ;
460+ } else {
461+ meta [ key ] = true ;
462+ }
463+ }
464+ } ) ;
465+ return meta ;
322466 }
323467} ;
324468
0 commit comments