From efc0fe81f003afcb3fdabe38bec27b1222462ecb Mon Sep 17 00:00:00 2001 From: William Mantly Date: Tue, 30 Jul 2024 20:38:22 -0400 Subject: [PATCH] LE class to manage wildcard cert --- nodejs/utils/letsencrypt.js | 135 ++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 nodejs/utils/letsencrypt.js diff --git a/nodejs/utils/letsencrypt.js b/nodejs/utils/letsencrypt.js new file mode 100644 index 0000000..b01bcc9 --- /dev/null +++ b/nodejs/utils/letsencrypt.js @@ -0,0 +1,135 @@ +'use strict'; + +const fs = require('fs') +const axios = require('axios'); +const AcmeClient = require('acme-client'); +const sleep = require('./sleep'); + +// https://dns.google/resolve?name=${name}&type=TXT + +class LetsEncrypt{ + static AcmeClient = AcmeClient; + + constructor(options){ + this.loadAccountKey(options.accountKeyPath || './le_key', (key)=>{ + this.client = new AcmeClient.Client({ + directoryUrl: options.directoryUrl || AcmeClient.directory.letsencrypt.production, + accountKey: key, + }); + }); + } + + loadAccountKey(accountKeyPath, cb){ + try{ + // Load the account key if it exists + cb(fs.readFileSync(accountKeyPath, 'utf8')); + }catch(error){ + if(error.code === 'ENOENT'){ + // Generate a new account key if it doesn't exist + AcmeClient.crypto.createPrivateKey().then(function(accountKey){ + fs.writeFileSync(accountKeyPath, accountKey.toString(), 'utf8'); + cb(accountKey.toString()); + }); + }else{ + throw error; + } + } + } + + async dnsWildcard(domain, options){ + /* + https://github.com/publishlab/node-acme-client/tree/master/examples/dns-01 + */ + + try{ + domain = domain.replace(/^\*\./, ''); + const [key, csr] = await AcmeClient.crypto.createCsr({ + altNames: [domain, `*.${domain}`], + }); + + const cert = await this.client.auto({ + csr, + email: 'wmantly@gmail.com', + termsOfServiceAgreed: true, + challengePriority: ['dns-01'], + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + // console.log(`start TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) + + let resCheck = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); + if(resCheck.data.Answer.some(record => record.data === keyAuthorization)) return; + + await options.challengeCreateFn(authz, challenge, keyAuthorization); + + let checkCount = 0; + while(true){ + await sleep(1500); + let res = await axios.get(`https://dns.google/resolve?name=_acme-challenge.${authz.identifier.value}&type=TXT`); + if(res.data.Answer.some(record => record.data === keyAuthorization)){ + // console.log(`found record for key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`) + break; + } + if(checkCount++ > 60) throw new Error('challengeCreateFn validation timed out'); + } + }, + challengeRemoveFn: options.challengeRemoveFn, + }); + + return { + key, + csr, + cert, + }; + + }catch(error){ + console.log('Error in LetsEncrypt.dnsChallenge', error) + } + } +} + +module.exports = LetsEncrypt; + +if(require.main === module){(async function(){try{ + + const tldExtract = require('tld-extract').parse_host; + const PorkBun = require('./porkbun'); + const conf = require('../conf/conf'); + + let porkBun = new PorkBun(conf.porkBun.apiKey, conf.porkBun.secretApiKey); + let letsEncrypt = new LetsEncrypt({ + directoryUrl: AcmeClient.directory.letsencrypt.staging, + }); + + let cert = await letsEncrypt.dnsWildcard('dev.test.holycore.quest', { + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + let parts = tldExtract(authz.identifier.value); + let res = await porkBun.createRecordForce(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`}); + }, + challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ + let parts = tldExtract(authz.identifier.value); + await porkBun.deleteRecords(parts.domain, {type:'TXT', name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, content: `${keyAuthorization}`}); + }, + }); + + console.log('IIFE csr', cert.csr.toString()) + // console.log(cert.cert.split('\n\n')[0]) + // console.log('IIFE cert info', +AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, 'IIFE cert:\n', String(cert.cert).split('\n\n') , /*'IIFE privKey\n', cert.key.toString()*/); +}catch(error){ + console.log('IIFE Error:', error) +}})()} + + + + + + + + + + + + + + + + +