2018-01-12 14:41:26 -05:00
/ * *
2018-01-15 00:01:06 -05:00
* @ description MeshCentral letsEncrypt module , uses GreenLock to do all the work .
2018-01-12 14:41:26 -05:00
* @ author Ylian Saint - Hilaire
2021-01-09 17:31:09 -05:00
* @ copyright Intel Corporation 2018 - 2021
2018-01-12 14:41:26 -05:00
* @ license Apache - 2.0
2018-01-15 00:01:06 -05:00
* @ version v0 . 0.2
2018-01-12 14:41:26 -05:00
* /
2018-08-29 20:40:30 -04:00
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
2019-11-14 01:47:17 -05:00
'use strict' ;
2018-08-27 15:24:15 -04:00
2020-03-05 04:39:40 -05:00
// ACME-Client Implementation
var globalLetsEncrypt = null ;
2020-03-11 19:53:09 -04:00
module . exports . CreateLetsEncrypt = function ( parent ) {
2020-03-05 04:39:40 -05:00
const acme = require ( 'acme-client' ) ;
var obj = { } ;
obj . fs = require ( 'fs' ) ;
obj . path = require ( 'path' ) ;
obj . parent = parent ;
obj . forge = obj . parent . certificateOperations . forge ;
obj . leDomains = null ;
obj . challenges = { } ;
obj . runAsProduction = false ;
obj . redirWebServerHooked = false ;
2020-03-05 14:18:50 -05:00
obj . configErr = null ;
obj . configOk = false ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
2020-03-05 14:18:50 -05:00
// Let's Encrypt debug logging
obj . log = function ( str ) {
parent . debug ( 'cert' , 'LE: ' + str ) ;
var d = new Date ( ) ;
obj . events . push ( d . toLocaleDateString ( ) + ' ' + d . toLocaleTimeString ( ) + ' - ' + str ) ;
while ( obj . events . length > 200 ) { obj . events . shift ( ) ; } // Keep only 200 last events.
}
obj . events = [ ] ;
2020-03-05 04:39:40 -05:00
// Setup the certificate storage paths
obj . certPath = obj . path . join ( obj . parent . datapath , 'letsencrypt-certs' ) ;
try { obj . parent . fs . mkdirSync ( obj . certPath ) ; } catch ( e ) { }
// Hook up GreenLock to the redirection server
if ( obj . parent . config . settings . rediraliasport === 80 ) { obj . redirWebServerHooked = true ; }
else if ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port == 80 ) ) { obj . redirWebServerHooked = true ; }
// Deal with HTTP challenges
function challengeCreateFn ( authz , challenge , keyAuthorization ) { if ( challenge . type === 'http-01' ) { obj . challenges [ challenge . token ] = keyAuthorization ; } }
function challengeRemoveFn ( authz , challenge , keyAuthorization ) { if ( challenge . type === 'http-01' ) { delete obj . challenges [ challenge . token ] ; } }
2020-03-06 17:06:33 -05:00
obj . challenge = function ( token , hostname , func ) { if ( obj . challenges [ token ] != null ) { obj . log ( "Succesful response to challenge." ) ; } else { obj . log ( "Failed to respond to challenge, token: " + token + ", table: " + JSON . stringify ( obj . challenges ) + "." ) ; } func ( obj . challenges [ token ] ) ; }
2020-03-05 04:39:40 -05:00
// Get the current certificate
obj . getCertificate = function ( certs , func ) {
obj . runAsProduction = ( obj . parent . config . letsencrypt . production === true ) ;
2020-03-05 14:18:50 -05:00
obj . log ( "Getting certs from local store (" + ( obj . runAsProduction ? "Production" : "Staging" ) + ")" ) ;
2020-12-28 18:21:50 -05:00
if ( certs . CommonName . indexOf ( '.' ) == - 1 ) { obj . configErr = "Add \"cert\" value to settings in config.json before using Let's Encrypt." ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . parent . config . letsencrypt == null ) { obj . configErr = "No Let's Encrypt configuration" ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . parent . config . letsencrypt . email == null ) { obj . configErr = "Let's Encrypt email address not specified." ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
if ( ( obj . parent . redirserver == null ) || ( ( typeof obj . parent . config . settings . rediraliasport === 'number' ) && ( obj . parent . config . settings . rediraliasport !== 80 ) ) || ( ( obj . parent . config . settings . rediraliasport == null ) && ( obj . parent . redirserver . port !== 80 ) ) ) { obj . configErr = "Redirection web server must be active on port 80 for Let's Encrypt to work." ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
if ( obj . redirWebServerHooked !== true ) { obj . configErr = "Redirection web server not setup for Let's Encrypt to work." ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
if ( ( obj . parent . config . letsencrypt . rsakeysize != null ) && ( obj . parent . config . letsencrypt . rsakeysize !== 2048 ) && ( obj . parent . config . letsencrypt . rsakeysize !== 3072 ) ) { obj . configErr = "Invalid Let's Encrypt certificate key size, must be 2048 or 3072." ; parent . addServerWarning ( obj . configErr ) ; obj . log ( "WARNING: " + obj . configErr ) ; func ( certs ) ; return ; }
2020-03-05 14:18:50 -05:00
if ( obj . checkInterval == null ) { obj . checkInterval = setInterval ( obj . checkRenewCertificate , 86400000 ) ; } // Call certificate check every 24 hours.
obj . configOk = true ;
2020-03-05 04:39:40 -05:00
// Get the list of domains
obj . leDomains = [ certs . CommonName ] ;
if ( obj . parent . config . letsencrypt . names != null ) {
if ( typeof obj . parent . config . letsencrypt . names == 'string' ) { obj . parent . config . letsencrypt . names = obj . parent . config . letsencrypt . names . split ( ',' ) ; }
obj . parent . config . letsencrypt . names . map ( function ( s ) { return s . trim ( ) ; } ) ; // Trim each name
if ( ( typeof obj . parent . config . letsencrypt . names != 'object' ) || ( obj . parent . config . letsencrypt . names . length == null ) ) { console . log ( "ERROR: Let's Encrypt names must be an array in config.json." ) ; func ( certs ) ; return ; }
obj . leDomains = obj . parent . config . letsencrypt . names ;
}
// Read TLS certificate from the configPath
var certFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.crt' : 'staging.crt' ) ) ;
var keyFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.key' : 'staging.key' ) ) ;
if ( obj . fs . existsSync ( certFile ) && obj . fs . existsSync ( keyFile ) ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Reading certificate files" ) ;
2020-03-05 04:39:40 -05:00
// Read the certificate and private key
var certPem = obj . fs . readFileSync ( certFile ) . toString ( 'utf8' ) ;
var cert = obj . forge . pki . certificateFromPem ( certPem ) ;
var keyPem = obj . fs . readFileSync ( keyFile ) . toString ( 'utf8' ) ;
var key = obj . forge . pki . privateKeyFromPem ( keyPem ) ;
// Decode the certificate common and alt names
obj . certNames = [ cert . subject . getField ( 'CN' ) . value ] ;
var altNames = cert . getExtension ( 'subjectAltName' ) ;
if ( altNames ) { for ( i = 0 ; i < altNames . altNames . length ; i ++ ) { var acn = altNames . altNames [ i ] . value . toLowerCase ( ) ; if ( obj . certNames . indexOf ( acn ) == - 1 ) { obj . certNames . push ( acn ) ; } } }
// Decode the certificate expire time
obj . certExpire = cert . validity . notAfter ;
// Use this certificate when possible on any domain
if ( obj . certNames . indexOf ( certs . CommonName ) >= 0 ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Setting LE cert for default domain." ) ;
2020-03-05 04:39:40 -05:00
certs . web . cert = certPem ;
certs . web . key = keyPem ;
//certs.web.ca = [results.pems.chain];
}
for ( var i in obj . parent . config . domains ) {
if ( ( obj . parent . config . domains [ i ] . dns != null ) && ( obj . parent . certificateOperations . compareCertificateNames ( obj . certNames , obj . parent . config . domains [ i ] . dns ) ) ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Setting LE cert for domain " + i + "." ) ;
2020-03-05 04:39:40 -05:00
certs . dns [ i ] . cert = certPem ;
certs . dns [ i ] . key = keyPem ;
//certs.dns[i].ca = [results.pems.chain];
}
}
} else {
2020-03-05 14:18:50 -05:00
obj . log ( "No certificate files found" ) ;
2020-03-05 04:39:40 -05:00
}
func ( certs ) ;
2020-03-05 14:18:50 -05:00
setTimeout ( obj . checkRenewCertificate , 5000 ) ; // Hold 5 seconds and check if we need to request a certificate.
2020-03-05 04:39:40 -05:00
}
// Check if we need to get a new certificate
// Return 0 = CertOK, 1 = Request:NoCert, 2 = Request:Expire, 3 = Request:MissingNames
obj . checkRenewCertificate = function ( ) {
2020-03-06 17:06:33 -05:00
if ( obj . pendingRequest == true ) { obj . log ( "Request for certificate is in process." ) ; return 4 ; }
2020-03-05 04:39:40 -05:00
if ( obj . certNames == null ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Got no certificates, asking for one now." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 1 ;
} else {
// Look at the existing certificate to see if we need to renew it
var daysLeft = Math . floor ( ( obj . certExpire - new Date ( ) ) / 86400000 ) ;
2020-03-05 14:18:50 -05:00
obj . log ( "Certificate has " + daysLeft + " day(s) left." ) ;
2020-03-05 04:39:40 -05:00
if ( daysLeft < 45 ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Asking for new certificate because of expire time." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 2 ;
} else {
var missingDomain = false ;
for ( var i in obj . leDomains ) {
if ( obj . parent . certificateOperations . compareCertificateNames ( obj . certNames , obj . leDomains [ i ] ) == false ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Missing name \"" + obj . leDomains [ i ] + "\"." ) ;
2020-03-05 04:39:40 -05:00
missingDomain = true ;
}
}
if ( missingDomain ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Asking for new certificate because of missing names." ) ;
2020-03-05 04:39:40 -05:00
obj . requestCertificate ( ) ;
return 3 ;
} else {
2020-03-05 14:18:50 -05:00
obj . log ( "Certificate is ok." ) ;
2020-03-05 04:39:40 -05:00
}
}
}
return 0 ;
}
obj . requestCertificate = function ( ) {
2020-03-06 17:06:33 -05:00
if ( obj . pendingRequest == true ) return ;
2021-03-08 04:41:12 -05:00
if ( obj . configOk == false ) { obj . log ( "Can't request cert, invalid configuration." ) ; return ; }
if ( acme . forge == null ) { obj . log ( "Forge not setup in ACME, unable to continue." ) ; return ; }
2020-03-06 17:06:33 -05:00
obj . pendingRequest = true ;
2020-03-05 14:18:50 -05:00
2020-03-05 04:39:40 -05:00
// Create a private key
2020-03-05 14:18:50 -05:00
obj . log ( "Generating private key..." ) ;
2020-03-05 04:39:40 -05:00
acme . forge . createPrivateKey ( ) . then ( function ( accountKey ) {
2020-11-24 05:39:32 -05:00
// TODO: ZeroSSL
// https://acme.zerossl.com/v2/DV90
2020-03-05 04:39:40 -05:00
// Create the ACME client
2020-03-05 14:18:50 -05:00
obj . log ( "Setting up ACME client..." ) ;
2020-03-05 04:39:40 -05:00
obj . client = new acme . Client ( {
directoryUrl : obj . runAsProduction ? acme . directory . letsencrypt . production : acme . directory . letsencrypt . staging ,
accountKey : accountKey
} ) ;
// Create Certificate Request (CSR)
2020-03-05 14:18:50 -05:00
obj . log ( "Creating certificate request..." ) ;
2020-11-17 22:30:26 -05:00
var certRequest = { commonName : obj . leDomains [ 0 ] } ;
if ( obj . leDomains . length > 1 ) { certRequest . altNames = obj . leDomains ; }
acme . forge . createCsr ( certRequest ) . then ( function ( r ) {
2020-03-05 04:39:40 -05:00
var csr = r [ 1 ] ;
obj . tempPrivateKey = r [ 0 ] ;
2020-03-05 14:18:50 -05:00
obj . log ( "Requesting certificate from Let's Encrypt..." ) ;
2020-03-05 04:39:40 -05:00
obj . client . auto ( {
csr ,
email : obj . parent . config . letsencrypt . email ,
termsOfServiceAgreed : true ,
challengeCreateFn ,
challengeRemoveFn
} ) . then ( function ( cert ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Got certificate." ) ;
2020-03-05 04:39:40 -05:00
// Save certificate and private key to PEM files
var certFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.crt' : 'staging.crt' ) ) ;
var keyFile = obj . path . join ( obj . certPath , ( obj . runAsProduction ? 'production.key' : 'staging.key' ) ) ;
obj . fs . writeFileSync ( certFile , cert ) ;
obj . fs . writeFileSync ( keyFile , obj . tempPrivateKey ) ;
delete obj . tempPrivateKey ;
// Cause a server restart
2020-03-05 14:18:50 -05:00
obj . log ( "Performing server restart..." ) ;
2020-03-05 04:39:40 -05:00
obj . parent . performServerCertUpdate ( ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to obtain certificate: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to generate certificate request: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
} , function ( err ) {
2020-03-05 14:18:50 -05:00
obj . log ( "Failed to generate private key: " + err . message ) ;
2020-03-06 17:06:33 -05:00
obj . pendingRequest = false ;
delete obj . client ;
2020-03-05 04:39:40 -05:00
} ) ;
}
// Return the status of this module
obj . getStats = function ( ) {
var r = {
2020-03-05 14:18:50 -05:00
configOk : obj . configOk ,
2020-03-05 04:39:40 -05:00
leDomains : obj . leDomains ,
challenges : obj . challenges ,
production : obj . runAsProduction ,
webServer : obj . redirWebServerHooked ,
2020-03-05 14:18:50 -05:00
certPath : obj . certPath
2020-03-05 04:39:40 -05:00
} ;
2020-12-28 18:21:50 -05:00
if ( obj . configErr ) { r . error = "WARNING: " + obj . configErr ; }
2020-03-05 14:18:50 -05:00
if ( obj . certExpire ) { r . cert = 'Present' ; r . daysLeft = Math . floor ( ( obj . certExpire - new Date ( ) ) / 86400000 ) ; } else { r . cert = 'None' ; }
2020-03-05 04:39:40 -05:00
return r ;
}
return obj ;
}