@@ -5,16 +5,18 @@ const _ = require('lodash');
5
5
const error = require ( '../lib/error' ) ;
6
6
const certificateModel = require ( '../models/certificate' ) ;
7
7
const internalAuditLog = require ( './audit-log' ) ;
8
- const internalHost = require ( './host' ) ;
9
8
const tempWrite = require ( 'temp-write' ) ;
10
9
const utils = require ( '../lib/utils' ) ;
10
+ const moment = require ( 'moment' ) ;
11
11
12
12
function omissions ( ) {
13
13
return [ 'is_deleted' ] ;
14
14
}
15
15
16
16
const internalCertificate = {
17
17
18
+ allowed_ssl_files : [ 'certificate' , 'certificate_key' , 'intermediate_certificate' ] ,
19
+
18
20
/**
19
21
* @param {Access } access
20
22
* @param {Object } data
@@ -57,8 +59,39 @@ const internalCertificate = {
57
59
update : ( access , data ) => {
58
60
return access . can ( 'certificates:update' , data . id )
59
61
. 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
+ } ) ;
62
95
} ) ;
63
96
} ,
64
97
@@ -113,10 +146,10 @@ const internalCertificate = {
113
146
} ,
114
147
115
148
/**
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]
120
153
* @returns {Promise }
121
154
*/
122
155
delete : ( access , data ) => {
@@ -134,6 +167,17 @@ const internalCertificate = {
134
167
. where ( 'id' , row . id )
135
168
. patch ( {
136
169
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
+ } ) ;
137
181
} ) ;
138
182
} )
139
183
. then ( ( ) => {
@@ -204,19 +248,18 @@ const internalCertificate = {
204
248
205
249
/**
206
250
* 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 .
208
252
*
209
- * @param {Access } access
210
253
* @param {Object } data
211
254
* @param {Object } data.files
212
255
* @returns {Promise }
213
256
*/
214
- validate : ( access , data ) => {
257
+ validate : data => {
215
258
return new Promise ( resolve => {
216
259
// Put file contents into an object
217
260
let files = { } ;
218
261
_ . map ( data . files , ( file , name ) => {
219
- if ( internalHost . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
262
+ if ( internalCertificate . allowed_ssl_files . indexOf ( name ) !== - 1 ) {
220
263
files [ name ] = file . data . toString ( ) ;
221
264
}
222
265
} ) ;
@@ -228,56 +271,26 @@ const internalCertificate = {
228
271
// Then test it depending on the file type
229
272
let promises = [ ] ;
230
273
_ . 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
+ } ) ) ;
260
284
} ) ;
261
285
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.
265
286
return Promise . all ( promises )
266
287
. then ( files => {
267
288
let data = { } ;
268
- let err = null ;
269
289
270
290
_ . each ( files , file => {
271
291
data = _ . assign ( { } , data , file ) ;
272
- if ( typeof file . err !== 'undefined' && file . err ) {
273
- err = file . err ;
274
- }
275
292
} ) ;
276
293
277
- if ( err ) {
278
- throw err ;
279
- }
280
-
281
294
return data ;
282
295
} ) ;
283
296
} ) ;
@@ -297,28 +310,159 @@ const internalCertificate = {
297
310
throw new error . ValidationError ( 'Cannot upload certificates for this type of provider' ) ;
298
311
}
299
312
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
+ }
305
318
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
+ } )
318
332
. 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 ) ;
320
358
} ) ;
321
359
} ) ;
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 ;
322
466
}
323
467
} ;
324
468
0 commit comments