Files
MikrotikManager/backend/scripts/acceptance/acme-renew-tls.mjs
T

72 lines
4.1 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import tls from 'node:tls';
import { X509Certificate } from 'node:crypto';
import { pathToFileURL } from 'node:url';
const fingerprint = value => {
assert.equal(typeof value, 'string', 'Fingerprint is required');
const normalized = value.replaceAll(':', '').toLowerCase();
assert(/^[a-f0-9]{64}$/.test(normalized), 'Expected SHA-256 fingerprint');
return normalized;
};
export function compareRenewal(before, after) {
for (const sample of [before, after]) {
assert(/^mmt253-[a-f0-9]{8}\.pngs\.top$/.test(sample.fqdn), 'Only the prepared exact test domain is permitted');
assert.equal(sample.remoteAddress, '104.165.244.7');
assert.equal(sample.authorized, true);
assert.equal(sample.hostnameMatched, true);
assert.equal(sample.nativePrivateKey, true);
assert.equal(sample.nativeName, sample.certName);
assert.equal(sample.certName, sample.fqdn.replace('.pngs.top', ''));
assert.equal(fingerprint(sample.nativeFingerprint256), fingerprint(sample.fingerprint256), 'Native leaf must match served TLS');
assert(Number.isFinite(Date.parse(sample.validUntil)), 'Expiry is required');
assert(Number.isFinite(Date.parse(sample.sampledAt)), 'Sample time is required');
assert(Date.parse(sample.validUntil) > Date.parse(sample.sampledAt), 'Certificate is expired');
}
assert.equal(after.fqdn, before.fqdn);
assert.equal(after.certName, before.certName);
assert.notEqual(fingerprint(after.fingerprint256), fingerprint(before.fingerprint256), 'Old certificate is still being served');
assert(Date.parse(after.sampledAt) >= Date.parse(before.sampledAt));
assert(Date.parse(after.validUntil) > Date.parse(after.sampledAt), 'New certificate is expired');
// A second issuance on the same day need not extend expiry by a full day.
return { fingerprintChanged: true, nativeAndTlsMatch: true, trustedHostnameMatch: true };
}
async function sample(fixture) {
assert(/^mmt253-[a-f0-9]{8}\.pngs\.top$/.test(fixture.fqdn));
assert.equal(fixture.certName, fixture.fqdn.replace('.pngs.top', ''));
assert.equal(fixture.nativePrivateKey, true);
fingerprint(fixture.nativeFingerprint256);
return await new Promise((resolve, reject) => {
const socket = tls.connect({ host: '104.165.244.7', port: 443, servername: fixture.fqdn, rejectUnauthorized: true });
socket.setTimeout(15000, () => socket.destroy(new Error('TLS acceptance timeout')));
socket.once('error', reject);
socket.once('secureConnect', () => {
try {
assert.equal(socket.authorized, true);
const cert = new X509Certificate(socket.getPeerCertificate().raw);
assert(cert.checkHost(fixture.fqdn), 'Hostname mismatch');
assert(Date.parse(cert.validTo) > Date.now(), 'Certificate is expired');
assert.equal(fingerprint(cert.fingerprint256), fingerprint(fixture.nativeFingerprint256), 'Native leaf does not match TLS');
resolve({ sampledAt: new Date().toISOString(), fqdn: fixture.fqdn, certName: fixture.certName,
remoteAddress: socket.remoteAddress, authorized: socket.authorized, hostnameMatched: true,
protocol: socket.getProtocol(), fingerprint256: cert.fingerprint256,
validFrom: cert.validFrom, validUntil: cert.validTo,
nativeName: fixture.certName, nativePrivateKey: true, nativeFingerprint256: fixture.nativeFingerprint256 });
} catch (error) { reject(error); } finally { socket.end(); }
});
});
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [mode, input, output] = process.argv.slice(2);
assert(input && output && ['sample', 'compare'].includes(mode), 'Usage: sample native-summary.json output.json | compare samples.json output.json');
assert(!fs.existsSync(output), 'Never overwrite acceptance evidence');
const data = JSON.parse(fs.readFileSync(input, 'utf8'));
const result = mode === 'sample' ? await sample(data) : compareRenewal(data.before, data.after);
fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n', { flag: 'wx' });
console.log(mode === 'sample' ? 'Trusted TLS/native fingerprint sample saved' : 'Renewal fingerprint comparison passed');
}