Delete all code related to GlobalSign sandcats certificates.

This commit is contained in:
Kenton Varda committed 2020-08-01 11:15:52 -05:00
1 parent 425e390d19
commit d0dcf5f6d4
3 files changed
-562

No files matched your search

-324
View File
@@ -6,23 +6,13 @@
// so that, along with pre-meteor.js, we create the the right
// HTTP+HTTPS services.
// Import modules provided by nodejs core.
var crypto = require('crypto');
var fs = require('fs');
var http = require('http');
var https = require('https');
var net = require('net');
var url = require('url');
var isDevShellMode = process.argv.length > 2;
var firstInheritedFd = isDevShellMode ? 65 : 3;
// Use the "node-forge" package from npm
var forge = isDevShellMode
? require('./shell/node_modules/node-forge')
: require('./programs/server/npm/node_modules/node-forge');
function sandstormMain() {
process.env.SANDSTORM_SMTP_LISTEN_HANDLE = (firstInheritedFd + 2).toString();
process.env.SANDSTORM_BACKEND_HANDLE = (firstInheritedFd + 1).toString();
@@ -95,320 +85,6 @@ function monkeypatchHttpForGateway() {
return oldListen.call(this, port, host, cb);
}
}
// TODO(cleanup): When in gateway mode, there's no reason for all the sandcats key-loading code
// to live here. But, the gateway is still optional today, and even when it becomes
// non-optional, refactoring this code is tricky because it's hard to test. For now we let it
// be and just give the main shell code the minimal hook it needs to invoke this code as
// necessary.
initSandcats();
}
// =======================================================================================
function makeHttpsDir(hostname) {
var httpsBasePath = '/var/sandcats/https';
if (! fs.existsSync(httpsBasePath)) {
fs.mkdirSync(httpsBasePath, 0700);
}
var hostnameDir = httpsBasePath + '/' + hostname;
if (! fs.existsSync(hostnameDir)) {
fs.mkdirSync(hostnameDir, 0700);
}
return hostnameDir;
};
function getNonSniKey() {
// Call this function to get a 'ca', 'cert', 'key' for browsers
// that don't support a HTTPS feature called Server Name
// Indication ("SNI").
// If we're lucky, we already have the files.
var hostname = 'client-does-not-support-sni.sandstorm-requires-sni.invalid';
var basePath = makeHttpsDir(hostname);
var keyBasename = "0.key";
var certBasename = "0.crt";
var keyFilename = basePath + "/" + keyBasename;
var certFilename = basePath + "/" + certBasename;
var files = fs.readdirSync(basePath);
if ((files.indexOf(keyBasename) == -1) ||
(files.indexOf(certBasename) == -1)) {
// Generate them synchronously. This could slow down the first
// start of a HTTPS-enabled Sandstorm.
console.log("Generating default HTTPS key for use with non-SNI clients. Expect a 45 second delay.");
// Generate 2048-bit key.
var keys = forge.pki.rsa.generateKeyPair({bits: 2048});
var keyAsText = forge.pki.privateKeyToPem(keys.privateKey);
fs.writeFileSync(keyFilename, keyAsText, "utf-8");
// Generate certificate.
var cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = '01';
cert.validity.notBefore = new Date("2015-09-01T00:00:00Z");
cert.validity.notAfter = new Date("2025-09-01T00:00:00Z");
var attribs = [{
name: 'commonName',
value: hostname
}];
cert.setSubject(attribs);
cert.setIssuer(attribs);
// Sign it with the same key.
cert.sign(keys.privateKey);
// Save it to disk.
var certAsText = forge.pki.certificateToPem(cert);
fs.writeFileSync(certFilename, certAsText, "utf-8");
console.log("Non-SNI key generation done.");
}
var keyAsText = fs.readFileSync(keyFilename, "utf-8");
var certAsText = fs.readFileSync(certFilename, "utf-8");
return {
ca: [],
key: keyAsText,
cert: certAsText
};
};
function getBestCertificate(now, files, basePath, preferOldest) {
// Get the newest certificate from disk, using "now" as a
// reference time by which to judge if a certificate is valid
// yet.
var validCertificates = [];
for (var i = 0; i < files.length; i++) {
var keyFilename = basePath + '/' + files[i];
var metadataFilename = keyFilename + '.response-json';
try {
var metadata = JSON.parse(fs.readFileSync(metadataFilename));
if (metadata.ca && metadata.ca.length === 2) {
// Our metadata files contain a field `ca` which contains the CA certificate and
// intermediate certificate. However, Node doesn't want this field; it wants the server
// certificate followed by the intermediate concatenated as one text blob. Qualys
// complains if we send the root cert since it's not useful -- the user needs to have
// it in their trust store anyway. ca[1] is always the intermediate cert, whereas ca[0]
// is the root.
// TODO(cleanup): Adjust the Sandcats server so that it doesn't send the `ca` field at
// all. This branch will then turn itself off.
metadata.cert = [metadata.cert, metadata.ca[1]].join("\r\n");
delete metadata.ca;
}
var validity = forge.pki.certificateFromPem(metadata['cert']).validity;
metadata.notBefore = Date.parse(validity.notBefore);
metadata.notAfter = Date.parse(validity.notAfter);
// Store this so we can log it.
metadata.keyFilename = keyFilename;
} catch (e) {
// Ignore EACCESS: The key metadata may have bad permissions
// due to an installer bug that went out during September.
//
// Ignore ENOENT: If we created a key+csr but the server
// hasn't given us a signed certificate yet, the response-json
// won't exist.
if ((e.code === 'EACCES') || (e.code == 'ENOENT')) {
console.log("Skipping unreadable HTTPS key information file while examining:",
metadataFilename);
continue;
} else {
// Sometimes the server gives us a 0-byte response,
// presumably due to a connection getting reset?.
console.error("Got exception reading JSON from file:", metadataFilename,
"Dazed and confused, but trying to continue.", e);
continue;
}
}
// If the cert is expired, definitely don't use it.
if (now > metadata.notAfter) {
console.log("Skipping", keyFilename, "because", metadata.notAfter, "is in the past.");
continue;
}
validCertificates.push(metadata);
}
// Sort by notBefore, descending (freshest first).
validCertificates.sort(function(cert1, cert2) {
return (cert2.notBefore - cert1.notBefore);
});
// If we prefer oldest, reverse that.
if (preferOldest) {
validCertificates = validCertificates.reverse();
}
return validCertificates[0];
}
function getCurrentSandcatsKeyAndNextRekeyTime() {
// Call this function to get up-to-date sandcats https key
// information.
//
// This returns an object with keys [ca, key, cert] which are
// valid options for https.createServer(). The object we return
// has one additional key, nextRekeyTime, which is a JS
// timestamp (UNIX time * 1000) of when the next key becomes
// valid.
//
// The intended use-case is that someone else will check the time
// and if the time is greater than nextRekeyTime, then call this
// function again.
//
// If nextRekeyTime is null, it means we have only one key.
var basePath = makeHttpsDir(url.parse(process.env.ROOT_URL).hostname);
// The key files in this directory are named 0 1 2 3 etc, but we
// ignore their filenames.
var files = fs.readdirSync(basePath);
// We might have lots and lots of valid certificates. We're
// interested in finding:
//
// - A "reference" certificate, which is the _newest_
// certificate that is currently valid. Newest is defined as
// the greatest NotBefore value.
//
// - If the "reference" certificate only started being valid in
// the past 20 minutes, then try to find a "temporary"
// certificate to use in that time, and schedule a re-key for
// when the "reference" certificate is worth using.
//
// - We should do all "Should we renew?" calculations based on
// the reference certificate, even if we aren't actually using
// it yet.
//
// We avoid using a cert so fresh as the past 20 minutes so that
// GlobalSign has some time to update OCSP.
var now = new Date().getTime();
var twentyMinutesAgo = now - (1000 * 60 * 20);
var referenceCertificate = getBestCertificate(now, files, basePath, false);
if (! referenceCertificate) {
console.error("HTTPS mode is enabled but no certs found.");
return {nextRekeyTime: null};
}
// By default, we will use the reference certificate immediately.
var useThisCertificate = referenceCertificate;
// We know that now > referenceCertificate.notBefore because it was
// returned to us.
//
// Has it been valid for the past 20 minutes? Meaning, is
// twentyMinutesAgo > referenceCertificate.notBefore? If it is,
// then great, let's definitely use the reference
// certificate. If not, then let's try to find a different cert.
if (twentyMinutesAgo > referenceCertificate.notBefore) {
// Great!
} else {
console.log("Looks like",
new Date(referenceCertificate.notBefore),
"started being fresh in the last 20 min, i.e. before",
new Date(twentyMinutesAgo));
// Look for the oldest cert we can. Maybe it'll be the same,
// maybe not, but at least we tried.
//
// This is useful if a Sandstorm restart occurs at an
// inopportune time, causing us to accidentally re-key.
var maybeUseThisCertificateInstead = getBestCertificate(now, files, basePath, true);
if (maybeUseThisCertificateInstead.keyFilename != referenceCertificate.keyFilename) {
useThisCertificate = maybeUseThisCertificateInstead;
}
}
console.log("Certificate we want to use:", referenceCertificate.keyFilename,
"valid starting", new Date(referenceCertificate.notBefore),
"until", new Date(referenceCertificate.notAfter));
if (useThisCertificate.keyFilename != referenceCertificate.keyFilename) {
console.log("Going to use this one for a little while first:",
useThisCertificate.keyFilename,
"valid starting", new Date(useThisCertificate.notBefore),
"until", new Date(useThisCertificate.notAfter));
}
// Store HTTPS configuration data from the cert we want to use.
//
// Also store bestCertExpiryTime (only looked-at within this
// file) based on the reference cert, even if we're not using it
// yet.
var result = {
ca: useThisCertificate.ca,
key: fs.readFileSync(useThisCertificate.keyFilename, 'utf-8'),
cert: useThisCertificate.cert,
notAfter: useThisCertificate.notAfter,
bestCertExpiryTime: referenceCertificate.notAfter
};
// Calculate re-key time.
//
// - If we're not using the reference certificate yet, then we
// should switch to it at the soonest of (20 min from now) and
// when the current cert expires (minus 20 min to avoid clock
// skew).
//
// - Otherwise, we know nothing about when to re-key, and we
// just sit here waiting for `sandcats.js` to download a new
// key for us to use, at which point we will either switch to
// it immediately or do the reference-cert-wait-20-min dance.
result.nextRekeyTime = null;
if (useThisCertificate.keyFilename != referenceCertificate.keyFilename) {
var twentyMinutesInMilliseconds = 1000 * 60 * 20;
var twentyMinutesFromNow = now + twentyMinutesInMilliseconds;
var notAfterMinusTwentyMinutes = result.notAfter - twentyMinutesInMilliseconds;
// But never attempt to re-key in the past...
if (notAfterMinusTwentyMinutes < now) {
notAfterMinusTwentyMinutes = result.notAfter;
}
result.nextRekeyTime = Math.min(
notAfterMinusTwentyMinutes, twentyMinutesFromNow);
console.log("Will switch certs, probably to", referenceCertificate.keyFilename,
", at time", new Date(result.nextRekeyTime),
"via real-time SNI re-keying system.");
} else {
console.log("We have only the one key that we know about. We'll need to renew soon.");
}
return result;
};
function initSandcats() {
// Create a global that others can call into if they want to
// re-key, such as when `sandcats.js` downloads a new
// certificate.
global.sandcats = {};
global.sandcats.state = {};
global.sandcats.rekey = function () {
global.sandcats.state = getCurrentSandcatsKeyAndNextRekeyTime();
};
global.sandcats.shouldGetAnotherCertificate = function() {
// Get a new certificate if our current cert (a) does not
// exist, or (b) has fewer than three days left.
if (! global.sandcats.state.key) {
console.log("shouldGetAnotherCertificate: There is no key, so yes get a new one.");
return true;
}
var threeDaysInMilliseconds = 1000 * 60 * 60 * 24 * 3;
var now = new Date();
var expiry = global.sandcats.state.bestCertExpiryTime;
var timeLeft = expiry - now.getTime();
if (timeLeft < threeDaysInMilliseconds) {
return true;
} else {
console.log("Since", now, "is more than three days away from",
new Date(expiry), "not renewing HTTPS cert yet.");
}
}
}
// =======================================================================================
sandstormMain();
-45
View File
@@ -284,26 +284,6 @@ function computeLetsEncryptSwitchDate(hostname) {
return new Date(ROLLOUT_START.getTime() + offset);
}
function switchSandcatsToLetsEncrypt(sandcatsName) {
// Check again that no one has created an account in the meantime.
if (!globalDb.getSetting("acmeAccount")) {
console.log("Automatically creating Let's Encrypt account. Sandcats certificates will be " +
"fetched from Let's Encrypt from now on.");
// As of this writing, mail sent to anything+letsencrypt@sandcats.io gets logged to a place
// only Kenton can see, but in the future we may make hostname[+anything]@sandcats.io forward
// to the owner of the hostname using their e-mail address on file. We'd need to implement some
// sort of e-mail verification before doing that, though.
createAcmeAccount("https://acme-v02.api.letsencrypt.org/directory",
sandcatsName + "+letsencrypt@sandcats.io", true);
let tlsKeys = globalDb.getSetting("tlsKeys");
if (tlsKeys && tlsKeys.certChain) {
renewCertificateWhenNeeded(tlsKeys.certChain);
}
}
}
// On replica 0, subscribe to updates to the `tlsKeys` setting and renew the certificate when
// needed.
if (!Meteor.settings.replicaNumber) {
@@ -318,30 +298,5 @@ if (!Meteor.settings.replicaNumber) {
removed(setting) { renewCertificateWhenNeeded(null); },
});
}, 0);
let sandcatsName = getSandcatsName();
if (sandcatsName && !globalDb.getSetting("acmeAccount")) {
// We're using old sandcats cert flow. Arrange to migrate to Let's Encrypt.
let timeout = computeLetsEncryptSwitchDate(sandcatsName).getTime() - Date.now();
if (timeout <= 0) {
// We were supposed to have switched already. Switch in one hour. The only reason we wait
// is because if this is a fresh install, the installer might immediately call us to set
// up ACME and we don't want to preempt that.
// TODO(cleanup): Next release, we delete the auto-migration code entirely!
timeout = 1 * HOURS;
}
if (timeout >= Math.pow(2, 31)) {
// Yikes, setTimeout() will do the wrong thing if we set this.
console.log("Error calculating Let's Encrypt auto-migration!", timeout);
} else {
console.log("Planning to migrate to Let's Encrypt automatically at:",
new Date(Date.now() + timeout));
Meteor.setTimeout(() => {
switchSandcatsToLetsEncrypt(sandcatsName);
}, timeout);
}
}
});
}
-193
View File
@@ -19,7 +19,6 @@ import { check } from "meteor/check";
import { Random } from "meteor/random";
import { inMeteor } from "/imports/server/async-helpers.ts";
import { pki, asn1 } from "node-forge";
import querystring from "querystring";
import https from "https";
import fs from "fs";
@@ -152,189 +151,7 @@ const performSandcatsRequest = (path, hostname, postData, errorCallback, respons
return req;
};
const generateKeyAndCsr = (commonName) => {
check(commonName, String);
// Generate key pair. Using Meteor.wrapAsync because forge supports
// a synchronous as well as an asynchronous API, and the synchronous
// one blocks for a while.
const wrappedGenerateKeyPair = Meteor.wrapAsync(pki.rsa.generateKeyPair);
// I could pick an `e`[xponent] value for the resulting RSA key, but
// I will refrain.
const keys = wrappedGenerateKeyPair({ bits: 2048 });
// Create a certificate request (CSR).
const csr = pki.createCertificationRequest();
csr.publicKey = keys.publicKey;
csr.setSubject([
{
name: "commonName",
value: commonName,
valueTagClass: asn1.Type.UTF8,
// We specify UTF8 to encode a UTF8String (rather than the default of PRINTABLESTRING) in the
// commonName so that GlobalSign does not report a warning, and also because that happens to
// be what openssl(1) does when asked to create a CSR.
},
]);
csr.sign(keys.privateKey);
console.log("generateKeyAndCsr created new key & certificate request for", commonName);
return {
privateKeyAsPem: pki.privateKeyToPem(keys.privateKey),
csrAsPem: pki.certificationRequestToPem(csr),
};
};
Sandcats.storeNewKeyAndCsr = (hostname, basePath) => {
// We use the current JS time (like UNIX timestamp but in
// milliseconds) as the key number. Note that the keyNumber is
// intended to be an opaque identifier; the only important thing is
// that it increases numerically over time. We use the current time
// just as a simplistic way to pick a filename that probably no one
// else has created yet.
const keyNumber = new Date().getTime();
const keyFilename = basePath + "/" + keyNumber;
const csrFilename = keyFilename + ".csr";
const responseFilename = keyFilename + ".response-json";
const withWildcard = "*." + hostname;
const keyAndCsr = generateKeyAndCsr(withWildcard);
fs.writeFileSync(keyFilename, keyAndCsr.privateKeyAsPem,
{ mode: 0o400 });
fs.writeFileSync(csrFilename, keyAndCsr.csrAsPem);
console.log("storeNewKeyAndCsr successfully saved key and certificate request to",
keyFilename, "and", csrFilename, "respectively of length",
keyAndCsr.privateKeyAsPem.length, "and",
keyAndCsr.csrAsPem.length);
return {
csrFilename: csrFilename,
keyFilename: keyFilename,
responseFilename: responseFilename,
};
};
function storeSandcatsCertToDb() {
if (globalDb.getSetting("acmeAccount")) {
// ACME has been enabled, don't overwrite the ACME-fetched key!
return;
}
globalDb.collections.settings.upsert({_id: "tlsKeys"}, { $set: {
value: {
key: sandcats.state.key,
certChain: [ sandcats.state.cert, sandcats.state.ca ].join("\n")
}
}});
}
Sandcats.renewHttpsCertificateIfNeeded = () => {
if (globalDb.getSetting("acmeAccount")) {
// ACME has been enabled, skip old Sandcats certificate flow!
return;
}
if (!global.sandcats) {
// global.sandcats may be missing when using `sandstorm dev-shell`. Skip certificate renewal in
// that case.
return;
}
const renewHttpsCertificate = () => {
const hostname = Url.parse(process.env.ROOT_URL).hostname;
const basePath = "/var/sandcats/https/" + (hostname);
const filenames = Sandcats.storeNewKeyAndCsr(hostname, basePath);
const errorCallback = (err) => {
console.error("Error while renewing HTTPS certificate (will continue to retry)", err);
};
const responseCallback = (res) => {
if (res.statusCode == 200) {
// Save the response, chunk by chunk, then store it on disk
// for later use.
let responseBody = "";
res.on("data", (chunk) => {
responseBody += chunk;
});
res.on("end", () => {
// For sanity, make sure it parses as JSON, since we're
// going to need it to do that.
try {
JSON.parse(responseBody);
} catch (e) {
console.error("JSON parse error receiving new HTTPS certificate. Discarding:",
responseBody,
"due to exception:",
e);
// Overwrite the responseBody with the empty JSON object
// and continue with the process of saving it to disk.
responseBody = "{}";
}
try {
fs.writeFileSync(filenames.responseFilename, responseBody, "utf-8");
} catch (err) {
return console.error("Failure while saveing new HTTPS certificate to",
filenames.responseFilename,
"will continue to retry. Exception was:",
err);
}
// Tell Sandcats that now is a good time to update its info
// about which keys are available.
if (!global.sandcats) {
console.error("When getting new certificate, could not find callback to request re-keying! Certs will probably become invalid soon.");
} else {
// Call the sandcats rekeying function.
global.sandcats.rekey();
inMeteor(storeSandcatsCertToDb);
// That's that.
console.log("Successfully renewed HTTPS certificate into",
filenames.responseFilename);
}
});
} else {
console.log("Received HTTP error while renewing certificate (will keep retrying)",
res.statusCode);
res.on("data", (chunk) => {
console.log("Error response contained information", chunk.toString("utf-8"));
});
}
};
const wrappedReadFile = Meteor.wrapAsync(fs.readFile);
performSandcatsRequest("/getcertificate", SANDCATS_HOSTNAME, {
rawHostname: SANDCATS_NAME,
certificateSigningRequest: wrappedReadFile(filenames.csrFilename, "utf-8"),
}, errorCallback, responseCallback);
};
if (global.sandcats.shouldGetAnotherCertificate()) {
console.log("renewHttpsCertificateIfNeeded: Happily choosing to renew certificate.");
renewHttpsCertificate();
}
if ((global.sandcats.state.nextRekeyTime !== null) &&
(Date.now() >= global.sandcats.state.nextRekeyTime)) {
// In gateway mode, we need to check periodically if it's time to swap certificates and then
// do it.
global.sandcats.rekey();
inMeteor(storeSandcatsCertToDb);
}
};
Sandcats.initializeSandcats = () => {
// global.sandcats may be missing when using `sandstorm dev-shell`. Don't bother refreshing keys
// from disk in that case.
if (global.sandcats) {
// The startup code doesn't automatically initialize sandcats, so call rekey() now to make that
// happen.
global.sandcats.rekey();
storeSandcatsCertToDb();
}
const i = HOSTNAME.lastIndexOf(SANDCATS_HOSTNAME);
if (i < 0) {
console.error("SANDCATS_BASE_DOMAIN is configured but your HOSTNAME doesn't appear to contain it:",
@@ -346,16 +163,6 @@ Sandcats.initializeSandcats = () => {
// All Sandcats installs need dyndns updating.
SANDCATS_NAME = HOSTNAME.slice(0, i - 1);
Meteor.setInterval(pingUdp, oneMinute);
// If process.env.HTTPS_PORT is set, we need to auto-refresh our HTTPS certificate.
if (process.env.HTTPS_PORT) {
// Always do a HTTPS certificate update check on Sandstorm start.
Sandcats.renewHttpsCertificateIfNeeded();
// After that's done, schedule it for every approx 1-2 hours in
// the future.
Meteor.setInterval(Sandcats.renewHttpsCertificateIfNeeded,
oneHour + randomIntervalZeroToOneHour);
}
}
};