UNPKG

meshcentral

Version:

Web based remote computer management server

832 lines (740 loc) • 87.1 kB
/** * @description Certificate generator * @author Joko Sastriawan / Ylian Saint-Hilaire * @copyright Intel Corporation 2018-2022 * @license Apache-2.0 * @version v0.0.1 */ /*xjslint node: true */ /*xjslint plusplus: true */ /*xjslint maxlen: 256 */ /*jshint node: true */ /*jshint strict: false */ /*jshint esversion: 6 */ "use strict"; module.exports.CertificateOperations = function (parent) { var obj = {}; obj.parent = parent; obj.fs = require('fs'); obj.forge = require('node-forge'); obj.crypto = require('crypto'); obj.tls = require('tls'); obj.pki = obj.forge.pki; obj.dirExists = function (filePath) { try { return obj.fs.statSync(filePath).isDirectory(); } catch (err) { return false; } }; obj.getFilesizeInBytes = function (filename) { try { return obj.fs.statSync(filename).size; } catch (err) { return -1; } }; const TopLevelDomainExtendedSupport = { 'net': 2, 'com': 2, 'arpa': 3, 'org': 2, 'gov': 2, 'edu': 2, 'de': 2, 'fr': 3, 'cn': 3, 'nl': 3, 'br': 3, 'mx': 3, 'uk': 3, 'pl': 3, 'tw': 3, 'ca': 3, 'fi': 3, 'be': 3, 'ru': 3, 'se': 3, 'ch': 2, 'dk': 2, 'ar': 3, 'es': 3, 'no': 3, 'at': 3, 'in': 3, 'tr': 3, 'cz': 2, 'ro': 3, 'hu': 3, 'nz': 3, 'pt': 3, 'il': 3, 'gr': 3, 'co': 3, 'ie': 3, 'za': 3, 'th': 3, 'sg': 3, 'hk': 3, 'cl': 2, 'lt': 3, 'id': 3, 'hr': 3, 'ee': 3, 'bg': 3, 'ua': 2 }; // Return true if the trusted FQDN matched the certificate common name function checkAcmActivationCertName(commonName, trustedFqdn) { commonName = commonName.toLowerCase(); trustedFqdn = trustedFqdn.toLowerCase(); if (commonName.startsWith('*.') && (commonName.length > 2)) { commonName = commonName.substring(2); } return ((commonName == trustedFqdn) || (trustedFqdn.endsWith('.' + commonName))); } // Sign a Intel AMT TLS ACM activation request obj.getAcmCertChain = function (domain, fqdn, hash) { if ((domain == null) || (domain.amtacmactivation == null) || (domain.amtacmactivation.certs == null) || (fqdn == null) || (hash == null)) return { action: 'acmactivate', error: 1, errorText: 'Invalid arguments' }; if (parent.common.validateString(fqdn, 4, 256) == false) return { action: 'acmactivate', error: 1, errorText: "Invalid FQDN argument." }; if (parent.common.validateString(hash, 16, 256) == false) return { action: 'acmactivate', error: 1, errorText: "Invalid hash argument." }; // Look for the signing certificate var signkey = null, certChain = null, hashAlgo = null, certIndex = null; for (var i in domain.amtacmactivation.certs) { const certEntry = domain.amtacmactivation.certs[i]; if ((certEntry.sha256 == hash) && ((certEntry.cn == '*') || checkAcmActivationCertName(certEntry.cn, fqdn))) { hashAlgo = 'sha256'; signkey = certEntry.key; certChain = certEntry.certs; certIndex = i; break; } if ((certEntry.sha1 == hash) && ((certEntry.cn == '*') || checkAcmActivationCertName(certEntry.cn, fqdn))) { hashAlgo = 'sha1'; signkey = certEntry.key; certChain = certEntry.certs; certIndex = i; break; } } if (signkey == null) return { action: 'acmactivate', error: 2, errorText: "Can't create ACM cert chain, no signing certificate found." }; // Did not find a match. // If the matching certificate our wildcard root cert, we can use the root to match any FQDN if (domain.amtacmactivation.certs[certIndex].cn == '*') { // Create a leaf certificate that matches the FQDN we want // TODO: This is an expensive operation, work on ways to pre-generate or cache this leaf certificate. var rootcert = { cert: domain.amtacmactivation.certs[certIndex].rootcert, key: obj.pki.privateKeyFromPem(domain.amtacmactivation.certs[certIndex].key) }; var leafcert = obj.IssueWebServerCertificate(rootcert, false, fqdn, 'mc', 'Intel(R) Client Setup Certificate', { serverAuth: true, '2.16.840.1.113741.1.2.3': true }, false); // Setup the certificate chain and key certChain = [ obj.pki.certificateToPem(leafcert.cert), obj.pki.certificateToPem(domain.amtacmactivation.certs[certIndex].rootcert) ]; signkey = obj.pki.privateKeyToPem(leafcert.key); } else { // Make sure the cert chain is in PEM format var certChain2 = []; for (var i in certChain) { certChain2.push("-----BEGIN CERTIFICATE-----\r\n" + certChain[i] + "\r\n-----END CERTIFICATE-----\r\n"); } certChain = certChain2; } // Hash the leaf certificate and return the certificate chain and signing key return { action: 'acmactivate', certs: certChain, signkey: signkey, hash384: obj.getCertHash(certChain[0]), hash256: obj.getCertHashSha256(certChain[0]) }; } // Sign a Intel AMT ACM activation request obj.signAcmRequest = function (domain, request, user, pass, ipport, nodeid, meshid, computerName, agentId) { if ((domain == null) || (domain.amtacmactivation == null) || (domain.amtacmactivation.certs == null) || (request == null) || (request.nonce == null) || (request.realm == null) || (request.fqdn == null) || (request.hash == null)) return { 'action': 'acmactivate', 'error': 1, 'errorText': 'Invalid arguments' }; if (parent.common.validateString(request.nonce, 16, 256) == false) return { 'action': 'acmactivate', 'error': 1, 'errorText': "Invalid nonce argument." }; if (parent.common.validateString(request.realm, 16, 256) == false) return { 'action': 'acmactivate', 'error': 1, 'errorText': "Invalid realm argument." }; if (parent.common.validateString(request.fqdn, 4, 256) == false) return { 'action': 'acmactivate', 'error': 1, 'errorText': "Invalid FQDN argument." }; if (parent.common.validateString(request.hash, 16, 256) == false) return { 'action': 'acmactivate', 'error': 1, 'errorText': "Invalid hash argument." }; if (parent.common.validateString(request.uuid, 36, 36) == false) return { 'action': 'acmactivate', 'error': 1, 'errorText': "Invalid UUID argument." }; // Look for the signing certificate var signkey = null, certChain = null, hashAlgo = null, certIndex = null; for (var i in domain.amtacmactivation.certs) { const certEntry = domain.amtacmactivation.certs[i]; if ((certEntry.sha256 == request.hash) && ((certEntry.cn == '*') || checkAcmActivationCertName(certEntry.cn, request.fqdn))) { hashAlgo = 'sha256'; signkey = certEntry.key; certChain = certEntry.certs; certIndex = i; break; } if ((certEntry.sha1 == request.hash) && ((certEntry.cn == '*') || checkAcmActivationCertName(certEntry.cn, request.fqdn))) { hashAlgo = 'sha1'; signkey = certEntry.key; certChain = certEntry.certs; certIndex = i; break; } } if (signkey == null) return { 'action': 'acmactivate', 'error': 2, 'errorText': "Can't sign ACM request, no signing certificate found." }; // Did not find a match. // If the matching certificate our wildcard root cert, we can use the root to match any FQDN if (domain.amtacmactivation.certs[certIndex].cn == '*') { // Create a leaf certificate that matches the FQDN we want // TODO: This is an expensive operation, work on ways to pre-generate or cache this leaf certificate. var rootcert = { cert: domain.amtacmactivation.certs[certIndex].rootcert, key: obj.pki.privateKeyFromPem(domain.amtacmactivation.certs[certIndex].key) }; var leafcert = obj.IssueWebServerCertificate(rootcert, false, request.fqdn, 'mc', 'Intel(R) Client Setup Certificate', { serverAuth: true, '2.16.840.1.113741.1.2.3': true }, false); // Setup the certificate chain and key certChain = [pemToBase64(obj.pki.certificateToPem(leafcert.cert)), pemToBase64(obj.pki.certificateToPem(domain.amtacmactivation.certs[certIndex].rootcert))]; signkey = obj.pki.privateKeyToPem(leafcert.key); } // Setup both nonces, ready to be signed const mcNonce = Buffer.from(obj.crypto.randomBytes(20), 'binary'); const fwNonce = Buffer.from(request.nonce, 'base64'); // Sign the request var signature = null; try { var signer = obj.crypto.createSign(hashAlgo); signer.update(Buffer.concat([fwNonce, mcNonce])); signature = signer.sign(signkey, 'base64'); } catch (ex) { return { 'action': 'acmactivate', 'error': 4, 'errorText': "Unable to perform signature." }; } // Log the activation request, logging is a required step for activation. if (obj.logAmtActivation(domain, { time: new Date(), action: 'acmactivate', domain: domain.id, amtUuid: request.uuid, certHash: request.hash, hashType: hashAlgo, amtRealm: request.realm, amtFqdn: request.fqdn, user: user, password: pass, ipport: ipport, nodeid: nodeid, meshid: meshid, computerName: computerName, agentId: agentId, tag: request.tag, name: request.name }) == false) return { 'action': 'acmactivate', 'error': 5, 'errorText': "Unable to log operation." }; // Return the signature with the computed account password hash return { 'action': 'acmactivate', 'signature': signature, 'password': obj.crypto.createHash('md5').update(user + ':' + request.realm + ':' + pass).digest('hex'), 'nonce': mcNonce.toString('base64'), 'certs': certChain }; } // Remove the PEM header, footer and carriage returns so we only have the Base64 DER. function pemToBase64(pem) { return pem.split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('').split('\r\n').join(''); } // Return true if both arrays match function compareArrays(a1, a2) { if (Array.isArray(a1) == false) return false; if (Array.isArray(a2) == false) return false; if (a1.length !== a2.length) return false; for (var i = 0; i < a1.length; i++) { if (a1[i] !== a2[i]) return false; } return true; } // Log the Intel AMT activation operation in the domain log obj.logAmtActivation = function (domain, x) { if (x == null) return true; // Add the password to the Intel AMT list of UUID to passwords if ((typeof x.amtUuid == 'string') && (typeof x.password == 'string')) { if (parent.amtPasswords == null) { parent.amtPasswords = {}; } if (parent.amtPasswords[x.amtUuid] == null) { parent.amtPasswords[x.amtUuid] = [x.password]; // Add password to array parent.amtPasswords = parent.common.sortObj(parent.amtPasswords); } else { if (parent.amtPasswords[x.amtUuid].indexOf(x.password) == -1) { parent.amtPasswords[x.amtUuid].unshift(x.password); // Add password at the start of the array while (parent.amtPasswords[x.amtUuid].length > 3) { parent.amtPasswords[x.amtUuid].pop(); } // Only keep the 3 last passwords for any given device } } } // Append to the log file var logpath = null; if ((domain.amtacmactivation == null) || (domain.amtacmactivation.log == null) || (typeof domain.amtacmactivation.log != 'string')) { if (domain.id == '') { logpath = parent.path.join(obj.parent.datapath, 'amtactivation.log'); } else { logpath = parent.path.join(obj.parent.datapath, 'amtactivation-' + domain.id + '.log'); } } else { logpath = parent.common.joinPath(obj.parent.datapath, domain.amtacmactivation.log); } try { obj.fs.appendFileSync(logpath, JSON.stringify(x) + '\r\n'); } catch (ex) { console.log(ex); return false; } return true; } // Load Intel AMT ACM activation certificates obj.loadIntelAmtAcmCerts = function (amtacmactivation) { if (amtacmactivation == null) return; var acmCerts = [], acmmatch = []; amtacmactivation.acmCertErrors = []; if (amtacmactivation.certs != null) { for (var j in amtacmactivation.certs) { if (j.startsWith('_')) continue; // Skip any certificates that start with underscore as the name. var acmconfig = amtacmactivation.certs[j], r = null; if ((typeof acmconfig.certpfx == 'string') && (typeof acmconfig.certpfxpass == 'string')) { // P12 format, certpfx and certpfxpass const certFilePath = parent.common.joinPath(obj.parent.datapath, acmconfig.certpfx); try { r = obj.loadPfxCertificate(certFilePath, acmconfig.certpfxpass); } catch (ex) { console.log(ex); } if ((r == null) || (r.certs == null) || (r.keys == null)) { amtacmactivation.acmCertErrors.push("Unable to load certificate file: " + certFilePath + "."); continue; } if (r.certs.length < 2) { amtacmactivation.acmCertErrors.push("Certificate file contains less then 2 certificates: " + certFilePath + "."); continue; } if (r.keys.length != 1) { amtacmactivation.acmCertErrors.push("Certificate file must contain exactly one private key: " + certFilePath + "."); continue; } } else if ((typeof acmconfig.certfiles == 'object') && (typeof acmconfig.keyfile == 'string')) { // PEM format, certfiles and keyfile r = { certs: [], keys: [] }; for (var k in acmconfig.certfiles) { const certFilePath = parent.common.joinPath(obj.parent.datapath, acmconfig.certfiles[k]); try { r.certs.push(obj.pki.certificateFromPem(obj.fs.readFileSync(certFilePath))); } catch (ex) { amtacmactivation.acmCertErrors.push("Unable to load certificate file: " + certFilePath + "."); } } r.keys.push(obj.pki.privateKeyFromPem(obj.fs.readFileSync(parent.common.joinPath(obj.parent.datapath, acmconfig.keyfile)))); if (r.certs.length < 2) { amtacmactivation.acmCertErrors.push("Certificate file contains less then 2 certificates: " + certFilePath + "."); continue; } if (r.keys.length != 1) { amtacmactivation.acmCertErrors.push("Certificate file must contain exactly one private key: " + certFilePath + "."); continue; } } // Reorder the certificates from leaf to root. var orderedCerts = [], or = [], currenthash = null, orderingError = false;; while ((orderingError == false) && (orderedCerts.length < r.certs.length)) { orderingError = true; for (var k in r.certs) { if (((currenthash == null) && (r.certs[k].subject.hash == r.certs[k].issuer.hash)) || ((r.certs[k].issuer.hash == currenthash) && (r.certs[k].subject.hash != r.certs[k].issuer.hash))) { currenthash = r.certs[k].subject.hash; orderedCerts.unshift(Buffer.from(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(r.certs[k])).data, 'binary').toString('base64')); or.unshift(r.certs[k]); orderingError = false; } } } if (orderingError == true) { amtacmactivation.acmCertErrors.push("Unable to order Intel AMT ACM activation certificates to create a full chain."); continue; } r.certs = or; // Check that the certificate and private key match if ((compareArrays(r.certs[0].publicKey.n.data, r.keys[0].n.data) == false) || (compareArrays(r.certs[0].publicKey.e.data, r.keys[0].e.data) == false)) { amtacmactivation.acmCertErrors.push("Intel AMT activation certificate provided with a mismatching private key."); continue; } /* // Debug: Display all certs & key as PEM for (var k in r.certs) { var cn = r.certs[k].subject.getField('CN'); if (cn != null) { console.log(cn.value + '\r\n' + obj.pki.certificateToPem(r.certs[k])); } else { console.log(obj.pki.certificateToPem(r.certs[k])); } } console.log(obj.pki.privateKeyToPem(r.keys[0])); */ // Check if the right OU or OID is present for Intel AMT activation var validActivationCert = false; for (var k in r.certs[0].extensions) { if (r.certs[0].extensions[k]['2.16.840.1.113741.1.2.3'] == true) { validActivationCert = true; } } var orgName = r.certs[0].subject.getField('OU'); if ((orgName != null) && (orgName.value == 'Intel(R) Client Setup Certificate')) { validActivationCert = true; } if (validActivationCert == false) { amtacmactivation.acmCertErrors.push("Intel AMT activation certificate must have usage OID \"2.16.840.1.113741.1.2.3\" or organization name \"Intel(R) Client Setup Certificate\"."); continue; } // Compute the SHA256 and SHA1 hashes of the root certificate for (var k in r.certs) { if (r.certs[k].subject.hash != r.certs[k].issuer.hash) continue; const certdata = obj.forge.asn1.toDer(obj.pki.certificateToAsn1(r.certs[k])).data; var md = obj.forge.md.sha256.create(); md.update(certdata); acmconfig.sha256 = Buffer.from(md.digest().getBytes(), 'binary').toString('hex'); md = obj.forge.md.sha1.create(); md.update(certdata); acmconfig.sha1 = Buffer.from(md.digest().getBytes(), 'binary').toString('hex'); } if ((acmconfig.sha1 == null) || (acmconfig.sha256 == null)) { amtacmactivation.acmCertErrors.push("Unable to compute Intel AMT activation certificate SHA1 and SHA256 hashes."); continue; } // Get the certificate common name var certCommonName = r.certs[0].subject.getField('CN'); if (certCommonName == null) { amtacmactivation.acmCertErrors.push("Unable to get Intel AMT activation certificate common name."); continue; } if (amtacmactivation.strictcommonname == true) { // Use the certificate common name exactly acmconfig.cn = certCommonName.value; } else { // Check if Intel AMT will allow some flexibility in the certificate common name var certCommonNameSplit = certCommonName.value.split('.'); var topLevel = certCommonNameSplit[certCommonNameSplit.length - 1].toLowerCase(); var topLevelNum = TopLevelDomainExtendedSupport[topLevel]; if (topLevelNum != null) { while (certCommonNameSplit.length > topLevelNum) { certCommonNameSplit.shift(); } acmconfig.cn = certCommonNameSplit.join('.'); } else { acmconfig.cn = certCommonName.value; } } if(r.certs[0].md){ acmconfig.hashAlgorithm = r.certs[0].md.algorithm; } delete acmconfig.cert; delete acmconfig.certpass; acmconfig.certs = orderedCerts; acmconfig.key = obj.pki.privateKeyToPem(r.keys[0]); acmCerts.push(acmconfig); acmmatch.push({ sha256: acmconfig.sha256, sha1: acmconfig.sha1, cn: acmconfig.cn }); } } amtacmactivation.acmmatch = acmmatch; amtacmactivation.certs = acmCerts; // Add the MeshCentral root cert as a possible activation cert if (obj.parent.certificates.root) { var x1 = obj.parent.certificates.root.cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = obj.parent.certificates.root.cert.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { var sha256 = obj.crypto.createHash('sha256').update(Buffer.from(obj.parent.certificates.root.cert.substring(x1 + 27, x2), 'base64')).digest('hex'); var sha1 = obj.crypto.createHash('sha1').update(Buffer.from(obj.parent.certificates.root.cert.substring(x1 + 27, x2), 'base64')).digest('hex'); amtacmactivation.certs.push({ 'sha256': sha256, 'sha1': sha1, 'cn': '*', rootcert: obj.pki.certificateFromPem(obj.parent.certificates.root.cert), key: obj.parent.certificates.root.key }); amtacmactivation.acmmatch.push({ 'sha256': sha256, 'sha1': sha1, 'cn': '*' }); } } } // Load a generic certificate and key from PFX/P12 or PEM format. Load both keys and attributes. obj.loadGenericCertAndKey = function (config) { if ((typeof config.certpfx == 'string') || (typeof config.certpfxpass == 'string')) { // Load a PFX certificate var r = null; try { r = obj.loadPfxCertificate(parent.getConfigFilePath(config.certpfx), config.certpfxpass); } catch (ex) { console.log(ex); } if ((r != null) && (r.keys.length > 0) && (r.certs.length > 0)) { var attributes = {}; for (var j in r.certs[0].subject.attributes) { attributes[r.certs[0].subject.attributes[j].shortName] = r.certs[0].subject.attributes[j].value; } return { cert: obj.pki.certificateToPem(r.certs[0]), key: obj.pki.privateKeyToPem(r.keys[0]), attributes: attributes }; } } if ((typeof config.certfile == 'string') || (typeof config.keyfile == 'string')) { // Load a PEM certificate var r = {} r.cert = obj.fs.readFileSync(parent.getConfigFilePath(config.certfile), 'utf8'); r.key = obj.fs.readFileSync(parent.getConfigFilePath(config.keyfile), 'utf8'); var cert = obj.pki.certificateFromPem(r.cert); r.attributes = {}; for (var j in cert.subject.attributes) { r.attributes[cert.subject.attributes[j].shortName] = cert.subject.attributes[j].value; } return r; } return null; } // Get the setup.bin file obj.GetSetupBinFile = function (amtacmactivation, oldmebxpass, newmebxpass, domain, user) { // Create a setup.bin file for our own root cert // Get the wiadcard certificate hash var wildcardCertSha256 = null; for (var i = 0; i < amtacmactivation.acmmatch.length; i++) { if (amtacmactivation.acmmatch[i].cn == '*') { wildcardCertSha256 = amtacmactivation.acmmatch[i].sha256; } } // Create the Setup.bin stack const AmtSetupBinStack = require('./amt/amt-setupbin')(); var setupbin = AmtSetupBinStack.AmtSetupBinCreate(3, 1); // Version 3, 1 = Records will not be consumed. var certRootName = 'MeshCentral'; // Figure out what trusted FQDN to use. var trustedFQDN = 'rootcert.meshcentral.com'; // Default DNS name. Any DNS name will do, we this is the fallback. if (typeof domain.dns == 'string') { // Use domain DNS name trustedFQDN = domain.dns; } else if (typeof parent.config.settings.cert == 'string') { // Use main DNS name trustedFQDN = parent.config.settings.cert; } // Create a new record var r = {}; r.typeIdentifier = 1; r.flags = 1; // Valid, unscrambled record. r.chunkCount = 0; r.headerByteCount = 0; r.number = 0; r.variables = []; setupbin.records.push(r); // Create "Current MEBx Password" variable var v = {}; v.moduleid = 1; v.varid = 1; v.length = -1; v.value = oldmebxpass; setupbin.records[0].variables.push(v); // Create "New MEBx Password" variable v = {}; v.moduleid = 1; v.varid = 2; v.length = -1; v.value = newmebxpass; setupbin.records[0].variables.push(v); // Create "User Defined Certificate Addition" variable v = {}; v.moduleid = 2; v.varid = 8; v.length = -1; v.value = String.fromCharCode(2) + Buffer.from(wildcardCertSha256, 'hex').toString('binary') + String.fromCharCode(certRootName.length) + certRootName; // 2 = SHA256 hash type setupbin.records[0].variables.push(v); // Create "PKI DNS Suffix" variable v = {}; v.moduleid = 2; v.varid = 3; v.length = -1; v.value = trustedFQDN; setupbin.records[0].variables.push(v); // Create "ME Provision Halt Active" variable v = {}; v.moduleid = 2; v.varid = 28; v.length = -1; v.value = 0; // Stop setupbin.records[0].variables.push(v); // Write to log file obj.logAmtActivation(domain, { time: new Date(), action: 'setupbin', domain: domain.id, userid: user._id, oldmebx: oldmebxpass, newmebx: newmebxpass, rootname: certRootName, hash: wildcardCertSha256, dns: trustedFQDN }); // Encode the setup.bin file return AmtSetupBinStack.AmtSetupBinEncode(setupbin); } // Get a bare metal setup.bin file obj.GetBareMetalSetupBinFile = function (amtacmactivation, oldmebxpass, newmebxpass, domain, user) { // Create a setup.bin file for our own root cert // Get the wiadcard certificate hash var wildcardCertSha256 = null; for (var i = 0; i < amtacmactivation.acmmatch.length; i++) { if (amtacmactivation.acmmatch[i].cn == '*') { wildcardCertSha256 = amtacmactivation.acmmatch[i].sha256; } } // Create the Setup.bin stack const AmtSetupBinStack = require('./amt/amt-setupbin')(); var setupbin = AmtSetupBinStack.AmtSetupBinCreate(3, 1); // Version 3, 1 = Records will not be consumed. var certRootName = 'MeshCentral'; // Figure out what trusted FQDN to use. var trustedFQDN = parent.config.settings.amtprovisioningserver.trustedfqdn // Figure out the provisioning server port var port = 9971; if (typeof parent.config.settings.amtprovisioningserver.port == 'number') { port = parent.config.settings.amtprovisioningserver.port; } // Get the provisioning server IP address from the config file if (typeof parent.config.settings.amtprovisioningserver.ip != 'string') return null; var ipaddr = parent.config.settings.amtprovisioningserver.ip; var ipaddrSplit = ipaddr.split('.'); var ipaddrStr = String.fromCharCode(parseInt(ipaddrSplit[3])) + String.fromCharCode(parseInt(ipaddrSplit[2])) + String.fromCharCode(parseInt(ipaddrSplit[1])) + String.fromCharCode(parseInt(ipaddrSplit[0])); // Create a new record var r = {}; r.typeIdentifier = 1; r.flags = 1; // Valid, unscrambled record. r.chunkCount = 0; r.headerByteCount = 0; r.number = 0; r.variables = []; setupbin.records.push(r); // Create "Current MEBx Password" variable var v = {}; v.moduleid = 1; v.varid = 1; v.length = -1; v.value = oldmebxpass; setupbin.records[0].variables.push(v); // Create "New MEBx Password" variable v = {}; v.moduleid = 1; v.varid = 2; v.length = -1; v.value = newmebxpass; setupbin.records[0].variables.push(v); // Create "User Defined Certificate Addition" variable v = {}; v.moduleid = 2; v.varid = 8; v.length = -1; v.value = String.fromCharCode(2) + Buffer.from(wildcardCertSha256, 'hex').toString('binary') + String.fromCharCode(certRootName.length) + certRootName; // 2 = SHA256 hash type setupbin.records[0].variables.push(v); // Create "PKI DNS Suffix" variable v = {}; v.moduleid = 2; v.varid = 3; v.length = -1; v.value = trustedFQDN; setupbin.records[0].variables.push(v); // Create "Configuration Server FQDN" variable v = {}; v.moduleid = 2; v.varid = 4; v.length = -1; v.value = trustedFQDN; setupbin.records[0].variables.push(v); // Create "Provisioning Server Address" variable v = {}; v.moduleid = 2; v.varid = 17; v.length = -1; v.value = ipaddrStr; setupbin.records[0].variables.push(v); // Create "Provisioning Server Port Number" variable v = {}; v.moduleid = 2; v.varid = 18; v.length = -1; v.value = port; setupbin.records[0].variables.push(v); // Create "ME Provision Halt Active" variable v = {}; v.moduleid = 2; v.varid = 28; v.length = -1; v.value = 1; // Start setupbin.records[0].variables.push(v); // Write to log file obj.logAmtActivation(domain, { time: new Date(), action: 'setupbin-bare-metal', domain: domain.id, userid: user._id, oldmebx: oldmebxpass, newmebx: newmebxpass, rootname: certRootName, hash: wildcardCertSha256, dns: trustedFQDN, ip: ipaddr, port: port }); // Encode the setup.bin file return AmtSetupBinStack.AmtSetupBinEncode(setupbin); } // Return the certificate of the remote HTTPS server obj.loadPfxCertificate = function (filename, password) { var r = { certs: [], keys: [] }; var pfxb64 = Buffer.from(obj.fs.readFileSync(filename)).toString('base64'); var pfx = obj.forge.pkcs12.pkcs12FromAsn1(obj.forge.asn1.fromDer(obj.forge.util.decode64(pfxb64)), true, password); // Get the certs from certbags var bags = pfx.getBags({ bagType: obj.forge.pki.oids.certBag }); for (var i = 0; i < bags[obj.forge.pki.oids.certBag].length; i++) { r.certs.push(bags[obj.forge.pki.oids.certBag][i].cert); } // Get shrouded key from key bags bags = pfx.getBags({ bagType: obj.forge.pki.oids.pkcs8ShroudedKeyBag }); for (var i = 0; i < bags[obj.forge.pki.oids.pkcs8ShroudedKeyBag].length; i++) { r.keys.push(bags[obj.forge.pki.oids.pkcs8ShroudedKeyBag][i].key); } return r; } // Return a text file from a remote HTTPS server obj.loadTextFile = function (url, tag, func) { const u = require('url').parse(url); if (u.protocol == 'https:') { // Read from HTTPS const https = require('https'); https.get(url, function(resp) { var data = ''; resp.on('data', function(chunk) { data += chunk; }); resp.on('end', function () { func(url, data, tag); }); resp.on('error', function (chunk) { func(url, null, tag); }); }).on('error', function (err) { func(url, null, tag); }); } else if (u.protocol == 'file:') { // Read a file obj.fs.readFile(url.substring(7), 'utf8', function (err, data) { func(url, err ? null : data, tag); }); } else { func(url, null, tag); } }; // Return the certificate of the remote HTTPS server obj.loadCertificate = function (url, hostname, tag, func) { const u = require('url').parse(url); if (u.protocol == 'https:') { // Read the certificate from HTTPS if (hostname == null) { hostname = u.hostname; } parent.debug('cert', "loadCertificate() - Loading certificate from " + u.hostname + ":" + (u.port ? u.port : 443) + ", Hostname: " + hostname + "..."); const tlssocket = obj.tls.connect((u.port ? u.port : 443), u.hostname, { servername: hostname, rejectUnauthorized: false }, function () { this.xxcert = this.getPeerCertificate(); parent.debug('cert', "loadCertificate() - TLS connected, " + ((this.xxcert != null) ? "got certificate." : "no certificate.")); try { this.destroy(); } catch (ex) { } this.xxfunc(this.xxurl, (this.xxcert == null)?null:(this.xxcert.raw.toString('binary')), hostname, this.xxtag); }); tlssocket.xxurl = url; tlssocket.xxfunc = func; tlssocket.xxtag = tag; tlssocket.on('error', function (error) { try { this.destroy(); } catch (ex) { } parent.debug('cert', "loadCertificate() - TLS error: " + error); this.xxfunc(this.xxurl, null, hostname, this.xxtag); }); } else if (u.protocol == 'file:') { // Read the certificate from a file obj.fs.readFile(url.substring(7), 'utf8', function (err, data) { if (err) { func(url, null, hostname, tag); return; } var x1 = data.indexOf('-----BEGIN CERTIFICATE-----'), x2 = data.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { func(url, Buffer.from(data.substring(x1 + 27, x2), 'base64').toString('binary'), hostname, tag); } else { func(url, data, hostname, tag); } }); } else { func(url, null, hostname, tag); } }; // Check if a configuration file exists obj.fileExists = function (filename) { if ((parent.configurationFiles != null) && (parent.configurationFiles[filename] != null)) { return true; } var filePath = parent.getConfigFilePath(filename); try { return obj.fs.statSync(filePath).isFile(); } catch (err) { return false; } }; // Load a configuration file obj.fileLoad = function (filename, encoding) { if ((parent.configurationFiles != null) && (parent.configurationFiles[filename] != null)) { if (typeof parent.configurationFiles[filename] == 'string') { return fixEndOfLines(parent.configurationFiles[filename]); } return fixEndOfLines(parent.configurationFiles[filename].toString()); } else { return fixEndOfLines(obj.fs.readFileSync(parent.getConfigFilePath(filename), encoding)); } } // Return the SHA384 hash of the certificate public key obj.getPublicKeyHash = function (cert) { var publickey = obj.pki.certificateFromPem(cert).publicKey; return obj.pki.getPublicKeyFingerprint(publickey, { encoding: 'hex', md: obj.forge.md.sha384.create() }); }; // Return the SHA1 hash of the certificate, return hex obj.getCertHashSha1 = function (cert) { try { var md = obj.forge.md.sha1.create(); md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes()); return md.digest().toHex(); } catch (ex) { // If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { return obj.crypto.createHash('sha1').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('hex'); } else { console.log("ERROR: Unable to decode certificate."); return null; } } }; // Return the SHA256 hash of the certificate, return hex obj.getCertHashSha256 = function (cert) { try { var md = obj.forge.md.sha256.create(); md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes()); return md.digest().toHex(); } catch (ex) { // If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { return obj.crypto.createHash('sha256').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('hex'); } else { console.log("ERROR: Unable to decode certificate."); return null; } } }; // Return the SHA384 hash of the certificate, return hex obj.getCertHash = function (cert) { try { var md = obj.forge.md.sha384.create(); md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes()); return md.digest().toHex(); } catch (ex) { // If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { return obj.crypto.createHash('sha384').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('hex'); } else { console.log("ERROR: Unable to decode certificate."); return null; } } }; // Return the SHA384 hash of the certificate public key obj.getPublicKeyHashBinary = function (pem) { const { X509Certificate } = require('crypto'); if (X509Certificate == null) { // This version of NodeJS (<v15.6.0) does not support X509 certs, use Node-Forge instead which only supports RSA certs. return obj.pki.getPublicKeyFingerprint(obj.pki.certificateFromPem(pem).publicKey, { encoding: 'binary', md: obj.forge.md.sha384.create() }); } else { // This version of NodeJS supports x509 certificates var cert = new X509Certificate(pem); return obj.crypto.createHash('sha384').update(cert.publicKey.export({ type: ((cert.publicKey.asymmetricKeyType == 'rsa') ? 'pkcs1' : 'spki'), format: 'der' })).digest('binary'); } }; // Return the SHA384 hash of the certificate, return binary obj.getCertHashBinary = function (cert) { try { // If this is a RSA certificate, we can use Forge to hash the ASN1 var md = obj.forge.md.sha384.create(); md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes()); return md.digest().getBytes(); } catch (ex) { // If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----'); if ((x1 >= 0) && (x2 > x1)) { return obj.crypto.createHash('sha384').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('binary'); } else { console.log("ERROR: Unable to decode certificate."); return null; } } }; // Create a self-signed certificate obj.GenerateRootCertificate = function (addThumbPrintToName, commonName, country, organization, strong) { var keys = obj.pki.rsa.generateKeyPair({ bits: (strong == true) ? 3072 : 2048, e: 0x10001 }); var cert = obj.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '' + require('crypto').randomBytes(4).readUInt32BE(0); cert.validity.notBefore = new Date(2018, 0, 1); cert.validity.notAfter = new Date(2049, 11, 31); if (addThumbPrintToName === true) { commonName += '-' + obj.pki.getPublicKeyFingerprint(cert.publicKey, { encoding: 'hex' }).substring(0, 6); } if (country == null) { country = "unknown"; } if (organization == null) { organization = "unknown"; } var attrs = [{ name: 'commonName', value: commonName }, { name: 'organizationName', value: organization }, { name: 'countryName', value: country }]; cert.setSubject(attrs); cert.setIssuer(attrs); // Create a root certificate //cert.setExtensions([{ name: 'basicConstraints', cA: true }, { name: 'nsCertType', sslCA: true, emailCA: true, objCA: true }, { name: 'subjectKeyIdentifier' }]); cert.setExtensions([{ name: 'basicConstraints', cA: true }, { name: 'subjectKeyIdentifier' }, { name: 'keyUsage', keyCertSign: true }]); cert.sign(keys.privateKey, obj.forge.md.sha384.create()); return { cert: cert, key: keys.privateKey }; }; // Issue a certificate from a root obj.IssueWebServerCertificate = function (rootcert, addThumbPrintToName, commonName, country, organization, extKeyUsage, strong) { var keys = obj.pki.rsa.generateKeyPair({ bits: (strong == true) ? 3072 : 2048, e: 0x10001 }); var cert = obj.pki.createCertificate(); cert.publicKey = keys.publicKey; cert.serialNumber = '' + require('crypto').randomBytes(4).readUInt32BE(0); cert.validity.notBefore = new Date(2018, 0, 1); cert.validity.notAfter = new Date(2049, 11, 31); if (addThumbPrintToName === true) { commonName += "-" + obj.pki.getPublicKeyFingerprint(cert.publicKey, { encoding: 'hex' }).substring(0, 6); } var attrs = [{ name: 'commonName', value: commonName }]; if (country != null) { attrs.push({ name: 'countryName', value: country }); } if (organization != null) { attrs.push({ name: 'organizationName', value: organization }); } cert.setSubject(attrs); cert.setIssuer(rootcert.cert.subject.attributes); if (extKeyUsage == null) { extKeyUsage = { name: 'extKeyUsage', serverAuth: true }; } else { extKeyUsage.name = 'extKeyUsage'; } //var extensions = [{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, keyEncipherment: true, dataEncipherment: true }, extKeyUsage, { name: "nsCertType", client: false, server: true, email: false, objsign: false, sslCA: false, emailCA: false, objCA: false }, { name: "subjectKeyIdentifier" }]; var extensions = [{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: true, dataEncipherment: (extKeyUsage.serverAuth !== true) }, extKeyUsage, { name: "subjectKeyIdentifier" }]; if (extKeyUsage.serverAuth === true) { // Set subjectAltName according to commonName parsing. // Ideally, we should let opportunity in given interface to set any type of altNames according to node_forge library // such as type 2, 6 and 7. (2 -> DNS, 6 -> URI, 7 -> IP) var altNames = []; // According to commonName parsing (IP or DNS), add URI and DNS and/or IP altNames if (require('net').isIP(commonName)) { // set both IP and DNS when commonName is an IP@ altNames.push({ type: 7, ip: commonName }); altNames.push({ type: 2, value: commonName }); } else { // set only DNS when commonName is a FQDN altNames.push({ type: 2, value: commonName }); } altNames.push({ type: 6, value: 'http://' + commonName + '/' }) // Add localhost stuff for easy testing on localhost ;) altNames.push({ type: 2, value: 'localhost' }); altNames.push({ type: 6, value: 'http://localhost/' }); altNames.push({ type: 7, ip: '127.0.0.1' }); extensions.push({ name: 'subjectAltName', altNames: altNames }); } if (extKeyUsage.codeSign === true) { extensions = [{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]; } cert.setExtensions(extensions); cert.sign(rootcert.key, obj.forge.md.sha384.create()); return { cert: cert, key: keys.privateKey }; }; // Make sure a string with Mac style CR endo of line is changed to Linux LF style. function fixEndOfLines(str) { if (typeof (str) != 'string') return str; // If this is not a string, do nothing. var i = str.indexOf('-----'); // Remove everything before "-----". if (i > 0) { str = str.substring(i); } // this solves problems with editors that save text file type indicators ahead of the text. if ((typeof(str) != 'string') || (str.indexOf('\n') > 0)) return str; // If there is a \n in the file, keep the file as-is. return str.split('\r').join('\n'); // If there is no \n, replace all \r with \n. } // Return true if the name is found in the certificates names, we support wildcard certificates obj.compareCertificateNames = function (certNames, name) { if (certNames == null) return false; name = name.toLowerCase(); var xcertNames = []; for (var i in certNames) { xcertNames.push(certNames[i].toLowerCase()); } if (xcertNames.indexOf(name) >= 0) return true; for (var i in xcertNames) { if ((xcertNames[i].startsWith('*.') == true) && (name.endsWith(xcertNames[i].substring(1)) == true)) { return true; } if (xcertNames[i].startsWith('http://*.') == true) { if (name.endsWith(xcertNames[i].substring(8)) == true) { return true; } if ((xcertNames[i].endsWith('/') == true) && (name.endsWith(xcertNames[i].substring(8, xcertNames[i].length - 1)) == true)) { return true; } } } return false; } // Return true if the certificate is valid obj.checkCertificate = function (pem, key) { const { X509Certificate } = require('crypto'); if (X509Certificate == null) { // This version of NodeJS (<v15.6.0) does not support X509 certs, use Node-Forge instead which only supports RSA certs. var cert = null; try { cert = obj.pki.certificateFromPem(pem); } catch (ex) { return false; } // Unable to decode certificate if (cert.serialNumber == '') return false; // Empty serial number is not allowed. } else { // This version of NodeJS supports x509 certificates try { const cert = new X509Certificate(pem); if ((cert.serialNumber == '') || (cert.serialNumber == null)) return false; // Empty serial number is not allowed. } catch (ex) { return false; } // Unable to decode certificate } return true; } // Get the Common Name from a certificate obj.getCertificateCommonName = function (pem, field) { if (field == null) { field = 'CN'; } const { X509Certificate } = require('crypto'); if (X509Certificate == null) { // This version of NodeJS (<v15.6.0) does not support X509 certs, use Node-Forge instead which only supports RSA certs. var cert = obj.pki.certificateFromPem(pem); if (cert.subject.getField(field) != null) return cert.subject.getField(field).value; } else { // This version of NodeJS supports x509 certificates const subjects = new X509Certificate(pem).subject.split('\n'); for (var i in subjects) { if (subjects[i].startsWith(field + '=')) { return subjects[i].substring(field.length + 1); } } } return null; } // Get the Issuer Common Name from a certificate obj.getCertificateIssuerCommonName = function (pem, field) { if (field == null) { field = 'CN'; } const { X509Certificate } = require('crypto'); if (X509Certificate == null) { // This version of NodeJS (<v15.6.0) does not support X509 certs, use Node-Forge instead which only supports RSA certs. var cert = obj.pki.certificateFromPem(pem); if (cert.issuer.getField(field) != null) return cert.issuer.getField(field).value; } else { // This version of NodeJS supports x509 certificates const subjects = new X509Certificate(pem).issuer.split('\n'); for (var i in subjects) { if (subjects[i].startsWith(field + '=')) { return subjects[i].substring(field.length + 1); } } } return null; } // Get the Common Name and alternate names from a certificate obj.getCertificateAltNames = function (pem) { const altNamesResults = []; const { X509Certificate } = require('crypto'); if (X509Certificate == null) { // This version of NodeJS (<v15.6.0) does not support X509 certs, use Node-Forge instead which only supports RSA certs. var cert = obj.pki.certificateFromPem(pem); if (cert.subject.getField('CN') != null) { altNamesResults.push(cert.subject.getField('CN').value); } var altNames = cert.getExtension('subjectAltName'); if (altNames) { for (i = 0; i < altNames.altNames.length; i++) { if ((altNames.altNames[i] != null) && (altNames.altNames[i].type === 2) && (typeof altNames.altNames[i].value === 'string')) { var acn = altNames.altNames[i].value.toLowerCase(); if (altNamesResults.indexOf(acn) == -1) { altNamesResults