From e90628316c5026a9c287f46ac2f49501595bfd67 Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Mon, 6 Jul 2026 10:30:51 -0500 Subject: [PATCH 1/4] Add CLI version of the tool --- bin/cli.js | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++ bin/tlsref.js | 23 ++++++ 2 files changed, 242 insertions(+) create mode 100644 bin/cli.js create mode 100755 bin/tlsref.js diff --git a/bin/cli.js b/bin/cli.js new file mode 100644 index 0000000..8beecbb --- /dev/null +++ b/bin/cli.js @@ -0,0 +1,219 @@ +import { parseArgs } from 'util'; +import configs from '../src/js/configs.js'; +import minver from '../src/js/helpers/minver.js'; +import { xmlEntities } from '../src/js/utils.js'; + +const guideln_latest = '6.0'; +const guidelines = {}; +const servers = Object.keys(configs).filter(k => k !== 'openssl'); + +async function fetch_guideline(guideln) { + if (isNaN(guideln) || isNaN(parseFloat(guideln))) { + return guideln_latest; + } + const url = `https://data.tlsref.org/guidelines/${guideln}.json`; + try { + const response = await fetch(url); + if (!response.ok) throw new Error(`error retrieving ${url}: ${response.status}`); + guidelines[guideln] = await response.json(); + return guideln; + } catch (error) { + process.stderr.write(`Warning: ${error.message}\n`); + return guideln_latest; + } +} + +function show_help() { + process.stdout.write( + 'Usage: tlsref --server --config [options]\n' + + '\n' + + 'Options:\n' + + ' -s, --server Server software (required)\n' + + ' -c, --config Configuration level: modern, intermediate, old (required)\n' + + ' -v, --version Server software version (default: latest)\n' + + ' -o, --openssl OpenSSL version (default: latest)\n' + + ' --hsts Enable HSTS (when supported)\n' + + ' --ocsp Enable OCSP stapling (when supported)\n' + + ' -g, --guideline Guideline version (default: ' + guideln_latest + ')\n' + + ' -h, --help Show this help message\n' + + '\n' + + 'Available servers: ' + servers.join(', ') + '\n' + ); +} + +async function main() { + const { values } = parseArgs({ + args: process.argv.slice(2), + options: { + server: { type: 'string', short: 's' }, + config: { type: 'string', short: 'c' }, + version: { type: 'string', short: 'v' }, + openssl: { type: 'string', short: 'o' }, + guideline: { type: 'string', short: 'g' }, + hsts: { type: 'boolean' }, + ocsp: { type: 'boolean' }, + help: { type: 'boolean', short: 'h' }, + }, + }); + + if (values.help) { + show_help(); + return; + } + + if (!values.server) { + process.stderr.write('Error: --server is required\n\n'); + show_help(); + process.exit(1); + } + + if (!servers.includes(values.server)) { + process.stderr.write('Error: unknown server \'' + values.server + '\'\n\nAvailable servers: ' + servers.join(', ') + '\n'); + process.exit(1); + } + + if (!values.config) { + process.stderr.write('Error: --config is required (modern, intermediate, old)\n'); + process.exit(1); + } + + const server = values.server; + const config = values.config; + const serverVersion = values.version || configs[server].latestVersion; + const opensslVersion = values.openssl || configs.openssl.latestVersion; + let guideln = values.guideline || guideln_latest; + + if (!guidelines[guideln]) { + guideln = await fetch_guideline(guideln); + if (guideln === '5.0') { + if (await fetch_guideline('5.1') === '5.1') { + for (let x of ['modern', 'intermediate', 'old']) { + let ss5 = guidelines['5.0'].configurations[x]; + ss5.ciphersuites = ss5.openssl_ciphersuites; + ss5.ciphers = { + iana: guidelines['5.1'].configurations[x].ciphers.iana, + openssl: ss5.openssl_ciphers, + }; + } + } else { + guideln = guideln_latest; + } + } + } + + const sstls = guidelines[guideln]; + const available_configs = Object.keys(sstls.configurations); + if (!available_configs.includes(config)) { + process.stderr.write('Error: config \'' + config + '\' is not available in guideline ' + guideln + ' (use: ' + available_configs.join(', ') + ')\n'); + process.exit(1); + } + const ssc = sstls.configurations[config]; + + const supportsOcspStapling = + configs[server].supportsOcspStapling + && minver(configs[server].supportsOcspStapling, serverVersion); + + const usesOpenssl = configs[server].usesOpenssl !== false; + + let fragment = 'server=' + server + '&version=' + serverVersion + '&config=' + config; + fragment += usesOpenssl ? '&openssl=' + opensslVersion : ''; + fragment += configs[server].supportsHsts !== false && values.hsts ? '&hsts' : ''; + fragment += supportsOcspStapling && values.ocsp ? '&ocsp' : ''; + fragment += '&guideline=' + guideln; + + let version_tags = configs[server].name + ' ' + serverVersion; + if (configs[server].eolBefore && !minver(configs[server].eolBefore, serverVersion)) { + version_tags += ' (UNSUPPORTED; end-of-life)'; + } + if (usesOpenssl) { + version_tags += ', OpenSSL ' + opensslVersion; + if (!minver(configs['openssl'].eolBefore, opensslVersion)) { + version_tags += ' (UNSUPPORTED; end-of-life)'; + } else if (!minver('3.5.0', opensslVersion) && minver('5.8', guideln)) { + version_tags += ' (OLD: missing PQC hybrid MLKEMs)'; + } + } + version_tags += ', ' + config + ' config'; + version_tags = xmlEntities(version_tags); + + const date = new Date().toISOString().substr(0, 10); + let header = 'generated ' + date + ', TLSRef Guideline v' + guideln + ', ' + version_tags; + header += configs[server].supportsHsts !== false && values.hsts ? ', HSTS' : ''; + header += supportsOcspStapling && values.ocsp ? ', OCSP' : ''; + + const link = 'https://configurator.tlsref.org/#' + fragment; + + let protocols = ssc.tls_versions; + if (!configs[server].tls13 + || !minver(configs[server].tls13, serverVersion) + || !minver(configs['openssl'].tls13, opensslVersion)) { + protocols = protocols.filter(p => p !== 'TLSv1.3'); + } + + let tlsCurves = ssc.tls_curves; + if (!minver('3.5.0', opensslVersion)) { + tlsCurves = tlsCurves.filter(g => g !== 'X25519MLKEM768'); + } + + const cipherFormat = configs[server].cipherFormat ? configs[server].cipherFormat : 'openssl'; + let ciphers = cipherFormat === 'go' ? ssc.ciphers['iana'] : ssc.ciphers[cipherFormat]; + const supportedCiphers = configs[server].supportedCiphers + ? configs[server].supportedCiphers + : cipherFormat === 'go' ? configs['go'].supportedCiphers : null; + if (supportedCiphers) { + ciphers = ciphers.filter(suite => supportedCiphers.indexOf(suite) !== -1); + } + if (ciphers.length && ciphers[0] === '@SECLEVEL=0') ciphers.shift(); + if (usesOpenssl && minver('3.0.0', opensslVersion)) { + if (protocols.includes('TLSv1.1')) ciphers.unshift('@SECLEVEL=0'); + } + + const form = { + config, + hsts: !!values.hsts && configs[server].supportsHsts !== false, + ocsp: !!values.ocsp && !!supportsOcspStapling, + opensslVersion, + server, + serverName: configs[server].name, + serverVersion, + version_tags, + }; + + const output = { + ciphers, + cipherSuites: ssc.ciphersuites, + date, + dhCommand: 'curl https://data.tlsref.org/ffdhe/ffdhe' + ssc.dh_param_size + '.txt', + dhParamSize: ssc.dh_param_size, + fragment, + hasVersions: configs[server].hasVersions !== false, + header, + hstsMaxAge: ssc.hsts_min_age, + hstsRedirectCode: 308, + latestVersion: configs[server].latestVersion, + link, + oldestClients: ssc.oldest_clients, + origin: 'https://configurator.tlsref.org', + protocols, + serverPreferredOrder: ssc.server_preferred_order, + showSupports: configs[server].showSupports !== false, + supportsHsts: configs[server].supportsHsts !== false, + supportsOcspStapling: !!supportsOcspStapling, + tlsCurves, + usesDhe: ciphers.join(':').includes(':DHE') || ciphers.join(':').includes('_DHE_'), + usesOpenssl, + }; + + if (protocols.length === 0) { + process.stderr.write('# unfortunately, ' + version_tags + ' is not supported with these software versions.\n'); + process.exit(1); + } + + const template = require('../src/js/helpers/' + server + '.js').default; + process.stdout.write(template(form, output)); +} + +main().catch(err => { + process.stderr.write('Error: ' + err.message + '\n'); + process.exit(1); +}); diff --git a/bin/tlsref.js b/bin/tlsref.js new file mode 100755 index 0000000..ac715fc --- /dev/null +++ b/bin/tlsref.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +'use strict'; + +const babel = require('@babel/core'); +const Module = require('module'); +const fs = require('fs'); + +// Hook into Node's module loader to transform ES module syntax in source files +const original = Module._extensions['.js']; +Module._extensions['.js'] = function (mod, filename) { + if (filename.includes('node_modules')) { + return original(mod, filename); + } + const code = fs.readFileSync(filename, 'utf8'); + const result = babel.transformSync(code, { + filename, + presets: [['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }]], + plugins: ['@babel/plugin-transform-object-rest-spread'], + }); + mod._compile(result.code, filename); +}; + +require('./cli.js'); From 8b2d5c53a3be70ac6d0de5eaba40af090e55a27e Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Mon, 6 Jul 2026 10:53:28 -0500 Subject: [PATCH 2/4] Fix style and re-add comments --- bin/cli.js | 57 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 8beecbb..52546ae 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -8,17 +8,21 @@ const guidelines = {}; const servers = Object.keys(configs).filter(k => k !== 'openssl'); async function fetch_guideline(guideln) { + // check for numerical version string, e.g. digit.digit if (isNaN(guideln) || isNaN(parseFloat(guideln))) { - return guideln_latest; + return guideln_latest; // invalid numerical version string } const url = `https://data.tlsref.org/guidelines/${guideln}.json`; try { const response = await fetch(url); - if (!response.ok) throw new Error(`error retrieving ${url}: ${response.status}`); + if (!response.ok) { + throw new Error(`error retrieving ${url}: ${response.status}`); + } + guidelines[guideln] = await response.json(); return guideln; } catch (error) { - process.stderr.write(`Warning: ${error.message}\n`); + console.error("Error: %s", error.message); return guideln_latest; } } @@ -62,18 +66,18 @@ async function main() { } if (!values.server) { - process.stderr.write('Error: --server is required\n\n'); + console.error('Error: --server is required'); show_help(); process.exit(1); } if (!servers.includes(values.server)) { - process.stderr.write('Error: unknown server \'' + values.server + '\'\n\nAvailable servers: ' + servers.join(', ') + '\n'); + console.error("Error: unknown server '%s'. Available servers: %s", values.server, servers.join(', ')); process.exit(1); } if (!values.config) { - process.stderr.write('Error: --config is required (modern, intermediate, old)\n'); + console.error('Error: --config is required (modern, intermediate, old)'); process.exit(1); } @@ -87,10 +91,11 @@ async function main() { guideln = await fetch_guideline(guideln); if (guideln === '5.0') { if (await fetch_guideline('5.1') === '5.1') { + // re-map keys from older guideline 5.0 for (let x of ['modern', 'intermediate', 'old']) { - let ss5 = guidelines['5.0'].configurations[x]; + let ss5 = guidelines['5.0'].configurations[x]; // server side tls config for that level ss5.ciphersuites = ss5.openssl_ciphersuites; - ss5.ciphers = { + ss5.ciphers = { // copy iana from 5.1 guideline iana: guidelines['5.1'].configurations[x].ciphers.iana, openssl: ss5.openssl_ciphers, }; @@ -104,7 +109,7 @@ async function main() { const sstls = guidelines[guideln]; const available_configs = Object.keys(sstls.configurations); if (!available_configs.includes(config)) { - process.stderr.write('Error: config \'' + config + '\' is not available in guideline ' + guideln + ' (use: ' + available_configs.join(', ') + ')\n'); + console.error("Error: config '%s' is not available in guideline %s (use: %s)", config, guideln, available_configs.join(', ')); process.exit(1); } const ssc = sstls.configurations[config]; @@ -115,43 +120,50 @@ async function main() { const usesOpenssl = configs[server].usesOpenssl !== false; - let fragment = 'server=' + server + '&version=' + serverVersion + '&config=' + config; - fragment += usesOpenssl ? '&openssl=' + opensslVersion : ''; + let fragment = `server=${server}&version=${serverVersion}&config=${config}`; + fragment += usesOpenssl ? `&openssl=${opensslVersion}` : ''; fragment += configs[server].supportsHsts !== false && values.hsts ? '&hsts' : ''; fragment += supportsOcspStapling && values.ocsp ? '&ocsp' : ''; - fragment += '&guideline=' + guideln; + fragment += `&guideline=${guideln}`; - let version_tags = configs[server].name + ' ' + serverVersion; - if (configs[server].eolBefore && !minver(configs[server].eolBefore, serverVersion)) { + let version_tags = `${configs[server].name} ${serverVersion}`; + if (configs[server].eolBefore + && !minver(configs[server].eolBefore, serverVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; } if (usesOpenssl) { - version_tags += ', OpenSSL ' + opensslVersion; + version_tags += `, OpenSSL ${opensslVersion}`; if (!minver(configs['openssl'].eolBefore, opensslVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; - } else if (!minver('3.5.0', opensslVersion) && minver('5.8', guideln)) { + } + else if (!minver('3.5.0', opensslVersion) + && minver('5.8', guideln)) { version_tags += ' (OLD: missing PQC hybrid MLKEMs)'; } } - version_tags += ', ' + config + ' config'; + version_tags += `, ${config} config`; + + // html-escape version_tags (even though version_tags is also used + // outside HTML contexts, HTML is not expected in version strings) version_tags = xmlEntities(version_tags); const date = new Date().toISOString().substr(0, 10); - let header = 'generated ' + date + ', TLSRef Guideline v' + guideln + ', ' + version_tags; + let header = `generated ${date}, TLSRef Guideline v${guideln}, ${version_tags}`; header += configs[server].supportsHsts !== false && values.hsts ? ', HSTS' : ''; header += supportsOcspStapling && values.ocsp ? ', OCSP' : ''; const link = 'https://configurator.tlsref.org/#' + fragment; + // we need to remove TLS 1.3 from the supported protocols if the software is too old let protocols = ssc.tls_versions; if (!configs[server].tls13 || !minver(configs[server].tls13, serverVersion) || !minver(configs['openssl'].tls13, opensslVersion)) { protocols = protocols.filter(p => p !== 'TLSv1.3'); } - let tlsCurves = ssc.tls_curves; if (!minver('3.5.0', opensslVersion)) { + // future: may need to filter 'X25519MLKEM768','SecP256r1MLKEM768','SecP384r1MLKEM1024' tlsCurves = tlsCurves.filter(g => g !== 'X25519MLKEM768'); } @@ -189,6 +201,7 @@ async function main() { hasVersions: configs[server].hasVersions !== false, header, hstsMaxAge: ssc.hsts_min_age, + //hstsRedirectCode: form['config'].value === 'old' ? 301 : 308, hstsRedirectCode: 308, latestVersion: configs[server].latestVersion, link, @@ -200,12 +213,14 @@ async function main() { supportsHsts: configs[server].supportsHsts !== false, supportsOcspStapling: !!supportsOcspStapling, tlsCurves, + // XXX: If DHE ciphers removed from guidelines, then usesDhe, dhCommand, + // dhParamSize, and helpers/*.js code which uses them can be removed usesDhe: ciphers.join(':').includes(':DHE') || ciphers.join(':').includes('_DHE_'), usesOpenssl, }; if (protocols.length === 0) { - process.stderr.write('# unfortunately, ' + version_tags + ' is not supported with these software versions.\n'); + console.error('# unfortunately, %s is not supported with these software versions.', version_tags); process.exit(1); } @@ -214,6 +229,6 @@ async function main() { } main().catch(err => { - process.stderr.write('Error: ' + err.message + '\n'); + console.error('Error: %s', err.message); process.exit(1); }); From 70cd37565a198ef945a716fad272c6b407af6bd8 Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Mon, 6 Jul 2026 13:44:41 -0500 Subject: [PATCH 3/4] Refactor to reuse code between Web and CLI interfaces --- bin/cli.js | 194 +++++++----------------------------------------- src/js/state.js | 144 ++++++++++++++++++++--------------- 2 files changed, 108 insertions(+), 230 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 52546ae..98040a5 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -2,31 +2,10 @@ import { parseArgs } from 'util'; import configs from '../src/js/configs.js'; import minver from '../src/js/helpers/minver.js'; import { xmlEntities } from '../src/js/utils.js'; +import { get_state } from '../src/js/state.js'; -const guideln_latest = '6.0'; -const guidelines = {}; const servers = Object.keys(configs).filter(k => k !== 'openssl'); -async function fetch_guideline(guideln) { - // check for numerical version string, e.g. digit.digit - if (isNaN(guideln) || isNaN(parseFloat(guideln))) { - return guideln_latest; // invalid numerical version string - } - const url = `https://data.tlsref.org/guidelines/${guideln}.json`; - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`error retrieving ${url}: ${response.status}`); - } - - guidelines[guideln] = await response.json(); - return guideln; - } catch (error) { - console.error("Error: %s", error.message); - return guideln_latest; - } -} - function show_help() { process.stdout.write( 'Usage: tlsref --server --config [options]\n' + @@ -62,173 +41,50 @@ async function main() { if (values.help) { show_help(); - return; + return 0; } if (!values.server) { console.error('Error: --server is required'); show_help(); - process.exit(1); + return 1; } if (!servers.includes(values.server)) { - console.error("Error: unknown server '%s'. Available servers: %s", values.server, servers.join(', ')); - process.exit(1); + throw new Error(`unknown server '${values.server}'. Available servers: ${servers.join(', ')}`); } if (!values.config) { - console.error('Error: --config is required (modern, intermediate, old)'); - process.exit(1); - } - - const server = values.server; - const config = values.config; - const serverVersion = values.version || configs[server].latestVersion; - const opensslVersion = values.openssl || configs.openssl.latestVersion; - let guideln = values.guideline || guideln_latest; - - if (!guidelines[guideln]) { - guideln = await fetch_guideline(guideln); - if (guideln === '5.0') { - if (await fetch_guideline('5.1') === '5.1') { - // re-map keys from older guideline 5.0 - for (let x of ['modern', 'intermediate', 'old']) { - let ss5 = guidelines['5.0'].configurations[x]; // server side tls config for that level - ss5.ciphersuites = ss5.openssl_ciphersuites; - ss5.ciphers = { // copy iana from 5.1 guideline - iana: guidelines['5.1'].configurations[x].ciphers.iana, - openssl: ss5.openssl_ciphers, - }; - } - } else { - guideln = guideln_latest; - } - } - } - - const sstls = guidelines[guideln]; - const available_configs = Object.keys(sstls.configurations); - if (!available_configs.includes(config)) { - console.error("Error: config '%s' is not available in guideline %s (use: %s)", config, guideln, available_configs.join(', ')); - process.exit(1); + throw new Error('--config is required (modern, intermediate, old)'); } - const ssc = sstls.configurations[config]; - - const supportsOcspStapling = - configs[server].supportsOcspStapling - && minver(configs[server].supportsOcspStapling, serverVersion); - const usesOpenssl = configs[server].usesOpenssl !== false; - - let fragment = `server=${server}&version=${serverVersion}&config=${config}`; - fragment += usesOpenssl ? `&openssl=${opensslVersion}` : ''; - fragment += configs[server].supportsHsts !== false && values.hsts ? '&hsts' : ''; - fragment += supportsOcspStapling && values.ocsp ? '&ocsp' : ''; - fragment += `&guideline=${guideln}`; - - let version_tags = `${configs[server].name} ${serverVersion}`; - if (configs[server].eolBefore - && !minver(configs[server].eolBefore, serverVersion)) { - version_tags += ' (UNSUPPORTED; end-of-life)'; - } - if (usesOpenssl) { - version_tags += `, OpenSSL ${opensslVersion}`; - if (!minver(configs['openssl'].eolBefore, opensslVersion)) { - version_tags += ' (UNSUPPORTED; end-of-life)'; - } - else if (!minver('3.5.0', opensslVersion) - && minver('5.8', guideln)) { - version_tags += ' (OLD: missing PQC hybrid MLKEMs)'; - } - } - version_tags += `, ${config} config`; - - // html-escape version_tags (even though version_tags is also used - // outside HTML contexts, HTML is not expected in version strings) - version_tags = xmlEntities(version_tags); - - const date = new Date().toISOString().substr(0, 10); - let header = `generated ${date}, TLSRef Guideline v${guideln}, ${version_tags}`; - header += configs[server].supportsHsts !== false && values.hsts ? ', HSTS' : ''; - header += supportsOcspStapling && values.ocsp ? ', OCSP' : ''; - - const link = 'https://configurator.tlsref.org/#' + fragment; - - // we need to remove TLS 1.3 from the supported protocols if the software is too old - let protocols = ssc.tls_versions; - if (!configs[server].tls13 - || !minver(configs[server].tls13, serverVersion) - || !minver(configs['openssl'].tls13, opensslVersion)) { - protocols = protocols.filter(p => p !== 'TLSv1.3'); - } - let tlsCurves = ssc.tls_curves; - if (!minver('3.5.0', opensslVersion)) { - // future: may need to filter 'X25519MLKEM768','SecP256r1MLKEM768','SecP384r1MLKEM1024' - tlsCurves = tlsCurves.filter(g => g !== 'X25519MLKEM768'); - } - - const cipherFormat = configs[server].cipherFormat ? configs[server].cipherFormat : 'openssl'; - let ciphers = cipherFormat === 'go' ? ssc.ciphers['iana'] : ssc.ciphers[cipherFormat]; - const supportedCiphers = configs[server].supportedCiphers - ? configs[server].supportedCiphers - : cipherFormat === 'go' ? configs['go'].supportedCiphers : null; - if (supportedCiphers) { - ciphers = ciphers.filter(suite => supportedCiphers.indexOf(suite) !== -1); - } - if (ciphers.length && ciphers[0] === '@SECLEVEL=0') ciphers.shift(); - if (usesOpenssl && minver('3.0.0', opensslVersion)) { - if (protocols.includes('TLSv1.1')) ciphers.unshift('@SECLEVEL=0'); + const { form, output } = await get_state({ + server: values.server, + config: values.config, + serverVersion: values.version, + opensslVersion: values.openssl, + guideln: values.guideline, + hsts: values.hsts, + ocsp: values.ocsp, + }); + if (!form || !output) { + return 1; } - const form = { - config, - hsts: !!values.hsts && configs[server].supportsHsts !== false, - ocsp: !!values.ocsp && !!supportsOcspStapling, - opensslVersion, - server, - serverName: configs[server].name, - serverVersion, - version_tags, - }; - - const output = { - ciphers, - cipherSuites: ssc.ciphersuites, - date, - dhCommand: 'curl https://data.tlsref.org/ffdhe/ffdhe' + ssc.dh_param_size + '.txt', - dhParamSize: ssc.dh_param_size, - fragment, - hasVersions: configs[server].hasVersions !== false, - header, - hstsMaxAge: ssc.hsts_min_age, - //hstsRedirectCode: form['config'].value === 'old' ? 301 : 308, - hstsRedirectCode: 308, - latestVersion: configs[server].latestVersion, - link, - oldestClients: ssc.oldest_clients, - origin: 'https://configurator.tlsref.org', - protocols, - serverPreferredOrder: ssc.server_preferred_order, - showSupports: configs[server].showSupports !== false, - supportsHsts: configs[server].supportsHsts !== false, - supportsOcspStapling: !!supportsOcspStapling, - tlsCurves, - // XXX: If DHE ciphers removed from guidelines, then usesDhe, dhCommand, - // dhParamSize, and helpers/*.js code which uses them can be removed - usesDhe: ciphers.join(':').includes(':DHE') || ciphers.join(':').includes('_DHE_'), - usesOpenssl, - }; - - if (protocols.length === 0) { - console.error('# unfortunately, %s is not supported with these software versions.', version_tags); - process.exit(1); + if (output.protocols.length === 0) { + process.stdout.write(`# unfortunately, ${form.version_tags} is not supported with these software versions.\n`); + return 1; } - const template = require('../src/js/helpers/' + server + '.js').default; + const template = require('../src/js/helpers/' + values.server + '.js').default; process.stdout.write(template(form, output)); + + return 0; } main().catch(err => { console.error('Error: %s', err.message); - process.exit(1); + return 1; +}).then(exitCode => { + process.exit(exitCode); }); diff --git a/src/js/state.js b/src/js/state.js index 39e1229..86db00c 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -6,34 +6,37 @@ import { xmlEntities } from './utils.js'; const guideln_latest = '6.0'; // update when guideline changes const guidelines = {}; -export default async function () { - - async function fetch_guideline(guideln) { - // check for numerical version string, e.g. digit.digit - if (isNaN(guideln) || isNaN(parseFloat(guideln))) { - return guideln_latest; // invalid numerical version string +async function fetch_guideline(guideln) { + // check for numerical version string, e.g. digit.digit + if (isNaN(guideln) || isNaN(parseFloat(guideln))) { + return guideln_latest; // invalid numerical version string + } + const url = `https://data.tlsref.org/guidelines/${guideln}.json`; + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`error retrieving ${url}: ${response.status}`); } - const url = "https://data.tlsref.org/guidelines/"+guideln+".json"; - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`error retrieving ${url}: ${response.status}`); - } - guidelines[guideln] = await response.json(); - return guideln; - } catch (error) { - console.error(error.message); - return guideln_latest; - } + guidelines[guideln] = await response.json(); + return guideln; + } catch (error) { + console.error("Error: %s", error.message); + return guideln_latest; + } +} + +export async function get_state({server, config, serverVersion, opensslVersion, guideln, hsts, ocsp, url = new URL('https://configurator.tlsref.org/'), gitrev = ''}) { + if (!serverVersion) { + serverVersion = configs[server].latestVersion; + } + if (!opensslVersion) { + opensslVersion = configs.openssl.latestVersion; + } + if (guideln === '') { + guideln = guideln_latest; } - const form = document.getElementById('form-generator').elements; - const config = form['config'].value; - const server = form['server'].value; - let guideln = form['guideline'].value !== '' - ? form['guideline'].value - : guideln_latest; let sstls = guidelines[guideln]; if (!sstls) { guideln = await fetch_guideline(guideln); @@ -56,37 +59,44 @@ export default async function () { // note: sstls.version for '5.0' is rendered as number 5, not string '5.0' sstls = guidelines[guideln]; } - const ssc = sstls.configurations[form['config'].value]; // server side tls config for that level + + const available_configs = Object.keys(sstls.configurations); + if (!available_configs.includes(config)) { + console.error("Error: config '%s' is not available in guideline %s (use: %s)", config, guideln, available_configs.join(', ')); + return { form: undefined, output: undefined }; + } + const ssc = sstls.configurations[config]; // server side tls config for that level const supportsOcspStapling = configs[server].supportsOcspStapling - && minver(configs[server].supportsOcspStapling, form['version'].value); - - const url = new URL(document.location); + && minver(configs[server].supportsOcspStapling, serverVersion); + + const usesOpenssl = configs[server].usesOpenssl !== false; + const supportsHsts = configs[server].supportsHsts !== false && hsts; // generate the fragment - let fragment = `server=${server}&version=${form['version'].value}&config=${config}`; - fragment += configs[server].usesOpenssl !== false ? `&openssl=${form['openssl'].value}` : ''; - fragment += configs[server].supportsHsts !== false && form['hsts'].checked ? '&hsts' : ''; - fragment += supportsOcspStapling && form['ocsp'].checked ? '&ocsp' : ''; + let fragment = `server=${server}&version=${serverVersion}&config=${config}`; + fragment += usesOpenssl ? `&openssl=${opensslVersion}` : ''; + fragment += supportsHsts ? '&hsts' : ''; + fragment += supportsOcspStapling && ocsp ? '&ocsp' : ''; fragment += `&guideline=${guideln}`; // generate the version tags - let version_tags = `${configs[server].name} ${form['version'].value}`; + let version_tags = `${configs[server].name} ${serverVersion}`; if (configs[server].eolBefore - && !minver(configs[server].eolBefore, form['version'].value)) { + && !minver(configs[server].eolBefore, serverVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; } if (configs[server].usesOpenssl !== false) { - version_tags += `, OpenSSL ${form['openssl'].value}`; - if (!minver(configs['openssl'].eolBefore, form['openssl'].value)) { + version_tags += `, OpenSSL ${opensslVersion}`; + if (!minver(configs['openssl'].eolBefore, opensslVersion)) { version_tags += ' (UNSUPPORTED; end-of-life)'; } - else if (!minver("3.5.0", form['openssl'].value) + else if (!minver("3.5.0", opensslVersion) && minver("5.8", guideln)) { version_tags += ' (OLD: missing PQC hybrid MLKEMs)'; } } - version_tags += `, ${form['config'].value} config`; + version_tags += `, ${config} config`; // html-escape version_tags (even though version_tags is also used // outside HTML contexts, HTML is not expected in version strings) @@ -95,9 +105,8 @@ export default async function () { // generate the header const date = new Date().toISOString().substr(0, 10); let header = `generated ${date}, TLSRef Guideline v${guideln}, ${version_tags}`; - header += configs[server].supportsHsts !== false && form['hsts'].checked ? ', HSTS' : ''; - header += supportsOcspStapling && form['ocsp'].checked ? ', OCSP' : ''; - const gitrev = document.getElementById('gitrev').value; + header += supportsHsts ? ', HSTS' : ''; + header += supportsOcspStapling && ocsp ? ', OCSP' : ''; header += gitrev ? `, gitrev=${gitrev}` : ''; const link = `${url.origin}${url.pathname}#${fragment}`; @@ -105,12 +114,12 @@ export default async function () { // we need to remove TLS 1.3 from the supported protocols if the software is too old let protocols = ssc.tls_versions; if (!configs[server].tls13 - || !minver(configs[server].tls13, form['version'].value) - || !minver(configs['openssl'].tls13, form['openssl'].value)) { + || !minver(configs[server].tls13, serverVersion) + || !minver(configs['openssl'].tls13, opensslVersion)) { protocols = protocols.filter(ciphers => ciphers !== 'TLSv1.3'); } let tlsCurves = ssc.tls_curves; - if (!minver('3.5.0', form['openssl'].value)) { + if (!minver('3.5.0', opensslVersion)) { // future: may need to filter 'X25519MLKEM768','SecP256r1MLKEM768','SecP384r1MLKEM1024' tlsCurves = tlsCurves.filter(groups => groups !== 'X25519MLKEM768'); } @@ -122,24 +131,22 @@ export default async function () { : cipherFormat === 'go' ? configs['go'].supportedCiphers : null; if (supportedCiphers) { ciphers = ciphers.filter(suite => supportedCiphers.indexOf(suite) !== -1); - } else { - ciphers = ciphers; } if (ciphers.length && ciphers[0] === '@SECLEVEL=0') ciphers.shift(); - if (configs[server].usesOpenssl !== false && minver('3.0.0', form['openssl'].value)) { + if (configs[server].usesOpenssl !== false && minver('3.0.0', opensslVersion)) { // set SECLEVEL=0 via cipher string to support TLSv1-1.1 "old" with OpenSSL 3.x if (protocols.includes('TLSv1.1')) ciphers.unshift('@SECLEVEL=0'); } - const state = { + return { form: { - config: form['config'].value, - hsts: form['hsts'].checked && configs[server].supportsHsts !== false, - ocsp: form['ocsp'].checked && supportsOcspStapling, - opensslVersion: form['openssl'].value, + config, + hsts: supportsHsts, + ocsp: ocsp && supportsOcspStapling, + opensslVersion, server, serverName: configs[server].name, - serverVersion: form['version'].value, + serverVersion, version_tags, }, output: { @@ -152,24 +159,39 @@ export default async function () { hasVersions: configs[server].hasVersions !== false, header, hstsMaxAge: ssc.hsts_min_age, - //hstsRedirectCode: form['config'].value === 'old' ? 301 : 308, + //hstsRedirectCode: config === 'old' ? 301 : 308, hstsRedirectCode: 308, latestVersion: configs[server].latestVersion, link, oldestClients: ssc.oldest_clients, origin: url.origin, - protocols: protocols, + protocols, serverPreferredOrder: ssc.server_preferred_order, showSupports: configs[server].showSupports !== false, supportsHsts: configs[server].supportsHsts !== false, - supportsOcspStapling: supportsOcspStapling, - tlsCurves: tlsCurves, + supportsOcspStapling, + tlsCurves, // XXX: If DHE ciphers removed from guidelines, then usesDhe, dhCommand, // dhParamSize, and helpers/*.js code which uses them can be removed - usesDhe: ciphers.join(":").includes(":DHE") || ciphers.join(":").includes("_DHE_"), - usesOpenssl: configs[server].usesOpenssl !== false, + usesDhe: ciphers.join(":").includes(":DHE") || ciphers.join(":").includes("_DHE_"), + usesOpenssl, }, - }; + } +} + +export default async function () { + + const form = document.getElementById('form-generator').elements; - return state; + return get_state({ + server: form['server'].value, + config: form['config'].value, + serverVersion: form['version'].value, + opensslVersion: form['openssl'].value, + guideln: form['guideline'].value, + hsts: !!form['hsts'].checked, + ocsp: !!form['ocsp'].checked, + url: new URL(document.location), + gitrev: document.getElementById('gitrev').value, + }) }; From 0e0b26f8034b5542720889eee1b78f7a9f5fb5ad Mon Sep 17 00:00:00 2001 From: "Stephen J. Butler" Date: Mon, 6 Jul 2026 14:27:17 -0500 Subject: [PATCH 4/4] Use default guideln for all falsy values, not just '' --- src/js/state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/state.js b/src/js/state.js index 86db00c..7421661 100644 --- a/src/js/state.js +++ b/src/js/state.js @@ -33,7 +33,7 @@ export async function get_state({server, config, serverVersion, opensslVersion, if (!opensslVersion) { opensslVersion = configs.openssl.latestVersion; } - if (guideln === '') { + if (!guideln) { guideln = guideln_latest; }