Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 servers = Object.keys(configs).filter(k => k !== 'openssl');

function show_help() {
process.stdout.write(
'Usage: tlsref --server <server> --config <config> [options]\n' +
'\n' +
'Options:\n' +
' -s, --server <server> Server software (required)\n' +
' -c, --config <config> Configuration level: modern, intermediate, old (required)\n' +
' -v, --version <version> Server software version (default: latest)\n' +
' -o, --openssl <version> OpenSSL version (default: latest)\n' +
' --hsts Enable HSTS (when supported)\n' +
' --ocsp Enable OCSP stapling (when supported)\n' +
' -g, --guideline <version> 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 0;
}

if (!values.server) {
console.error('Error: --server is required');
show_help();
return 1;
}

if (!servers.includes(values.server)) {
throw new Error(`unknown server '${values.server}'. Available servers: ${servers.join(', ')}`);
}

if (!values.config) {
throw new Error('--config is required (modern, intermediate, old)');
}

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;
}

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/' + values.server + '.js').default;
process.stdout.write(template(form, output));

return 0;
}

main().catch(err => {
console.error('Error: %s', err.message);
return 1;
}).then(exitCode => {
process.exit(exitCode);
});
23 changes: 23 additions & 0 deletions bin/tlsref.js
Original file line number Diff line number Diff line change
@@ -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');
144 changes: 83 additions & 61 deletions src/js/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
Expand All @@ -95,22 +105,21 @@ 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}`;

// 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');
}
Expand All @@ -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: {
Expand All @@ -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,
})
};