2019-10-20 08:51:19 +00:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var C = module.exports;
|
|
|
|
var U = require('./utils.js');
|
|
|
|
var CSR = require('@root/csr');
|
|
|
|
var Enc = require('@root/encoding');
|
2019-10-27 10:38:05 +00:00
|
|
|
var Keypairs = require('@root/keypairs');
|
2019-10-20 08:51:19 +00:00
|
|
|
|
|
|
|
var pending = {};
|
|
|
|
var rawPending = {};
|
|
|
|
|
2019-10-27 10:38:05 +00:00
|
|
|
// What the abbreviations mean
|
|
|
|
//
|
|
|
|
// gnlkc => greenlock
|
|
|
|
// mconf => manager config
|
|
|
|
// db => greenlock store instance
|
|
|
|
// acme => instance of ACME.js
|
|
|
|
// chs => instances of challenges
|
|
|
|
// acc => account
|
|
|
|
// args => site / extra options
|
|
|
|
|
2019-10-20 08:51:19 +00:00
|
|
|
// Certificates
|
2019-10-27 10:38:05 +00:00
|
|
|
C._getOrOrder = function(gnlck, mconf, db, acme, chs, acc, args) {
|
|
|
|
var email =
|
|
|
|
args.subscriberEmail ||
|
|
|
|
mconf.subscriberEmail ||
|
|
|
|
gnlck._defaults.subscriberEmail;
|
2019-10-20 08:51:19 +00:00
|
|
|
|
|
|
|
var id = args.altnames.join(' ');
|
|
|
|
if (pending[id]) {
|
|
|
|
return pending[id];
|
|
|
|
}
|
|
|
|
|
|
|
|
pending[id] = C._rawGetOrOrder(
|
2019-10-27 10:38:05 +00:00
|
|
|
gnlck,
|
|
|
|
mconf,
|
2019-10-20 08:51:19 +00:00
|
|
|
db,
|
|
|
|
acme,
|
2019-10-27 10:38:05 +00:00
|
|
|
chs,
|
|
|
|
acc,
|
2019-10-20 08:51:19 +00:00
|
|
|
email,
|
|
|
|
args
|
|
|
|
)
|
|
|
|
.then(function(pems) {
|
|
|
|
delete pending[id];
|
|
|
|
return pems;
|
|
|
|
})
|
|
|
|
.catch(function(err) {
|
|
|
|
delete pending[id];
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
|
|
|
|
return pending[id];
|
|
|
|
};
|
|
|
|
|
|
|
|
// Certificates
|
2019-10-27 10:38:05 +00:00
|
|
|
C._rawGetOrOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) {
|
|
|
|
return C._check(gnlck, mconf, db, args).then(function(pems) {
|
2019-10-20 08:51:19 +00:00
|
|
|
// Nice and fresh? We're done!
|
2019-10-29 05:18:13 +00:00
|
|
|
if (pems) {
|
|
|
|
if (!C._isStale(gnlck, mconf, args, pems)) {
|
|
|
|
// return existing unexpired (although potentially stale) certificates when available
|
|
|
|
// there will be an additional .renewing property if the certs are being asynchronously renewed
|
|
|
|
//pems._type = 'current';
|
|
|
|
return pems;
|
|
|
|
}
|
2019-10-20 08:51:19 +00:00
|
|
|
}
|
|
|
|
|
2019-10-29 05:18:13 +00:00
|
|
|
// We're either starting fresh or freshening up...
|
|
|
|
var p = C._rawOrder(gnlck, mconf, db, acme, chs, acc, email, args);
|
|
|
|
var evname = pems ? 'cert_renewal' : 'cert_issue';
|
|
|
|
p.then(function(newPems) {
|
|
|
|
// notify in the background
|
2019-10-29 06:16:51 +00:00
|
|
|
var renewAt = C._renewWithStagger(gnlck, mconf, args, newPems);
|
2019-10-29 05:18:13 +00:00
|
|
|
gnlck._notify(evname, {
|
|
|
|
renewAt: renewAt,
|
|
|
|
subject: args.subject,
|
|
|
|
altnames: args.altnames
|
|
|
|
});
|
2019-10-29 06:16:51 +00:00
|
|
|
gnlck._notify('_cert_issue', {
|
|
|
|
renewAt: renewAt,
|
|
|
|
subject: args.subject,
|
|
|
|
altnames: args.altnames,
|
|
|
|
pems: newPems
|
|
|
|
});
|
2019-10-29 05:18:13 +00:00
|
|
|
}).catch(function(err) {
|
|
|
|
if (!err.context) {
|
|
|
|
err.context = evname;
|
2019-10-27 10:38:05 +00:00
|
|
|
}
|
2019-10-29 05:18:13 +00:00
|
|
|
err.subject = args.subject;
|
|
|
|
err.altnames = args.altnames;
|
|
|
|
gnlck._notify('error', err);
|
|
|
|
});
|
|
|
|
|
|
|
|
// No choice but to hang tight and wait for it
|
|
|
|
if (!pems) {
|
|
|
|
return p;
|
|
|
|
}
|
2019-10-20 08:51:19 +00:00
|
|
|
|
2019-10-29 05:18:13 +00:00
|
|
|
// Wait it out
|
|
|
|
// TODO should we call this waitForRenewal?
|
2019-10-20 08:51:19 +00:00
|
|
|
if (args.waitForRenewal) {
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
2019-10-29 05:18:13 +00:00
|
|
|
// Let the certs renew in the background
|
2019-10-20 08:51:19 +00:00
|
|
|
return pems;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// we have another promise here because it the optional renewal
|
|
|
|
// may resolve in a different stack than the returned pems
|
2019-10-27 10:38:05 +00:00
|
|
|
C._rawOrder = function(gnlck, mconf, db, acme, chs, acc, email, args) {
|
2019-10-20 08:51:19 +00:00
|
|
|
var id = args.altnames
|
|
|
|
.slice(0)
|
|
|
|
.sort()
|
|
|
|
.join(' ');
|
|
|
|
if (rawPending[id]) {
|
|
|
|
return rawPending[id];
|
|
|
|
}
|
|
|
|
|
2019-10-27 10:38:05 +00:00
|
|
|
var keyType =
|
|
|
|
args.serverKeyType ||
|
|
|
|
mconf.serverKeyType ||
|
|
|
|
gnlck._defaults.serverKeyType;
|
2019-10-20 08:51:19 +00:00
|
|
|
var query = {
|
|
|
|
subject: args.subject,
|
2019-10-27 10:38:05 +00:00
|
|
|
certificate: args.certificate || {},
|
|
|
|
directoryUrl:
|
|
|
|
args.directoryUrl ||
|
|
|
|
mconf.directoryUrl ||
|
|
|
|
gnlck._defaults.directoryUrl
|
2019-10-20 08:51:19 +00:00
|
|
|
};
|
|
|
|
rawPending[id] = U._getOrCreateKeypair(db, args.subject, query, keyType)
|
|
|
|
.then(function(kresult) {
|
|
|
|
var serverKeypair = kresult.keypair;
|
|
|
|
var domains = args.altnames.slice(0);
|
|
|
|
|
|
|
|
return CSR.csr({
|
2019-10-27 10:38:05 +00:00
|
|
|
jwk: serverKeypair.privateKeyJwk || serverKeypair.private,
|
2019-10-20 08:51:19 +00:00
|
|
|
domains: domains,
|
|
|
|
encoding: 'der'
|
|
|
|
})
|
|
|
|
.then(function(csrDer) {
|
|
|
|
// TODO let CSR support 'urlBase64' ?
|
|
|
|
return Enc.bufToUrlBase64(csrDer);
|
|
|
|
})
|
|
|
|
.then(function(csr) {
|
2019-10-28 08:25:32 +00:00
|
|
|
function notify(ev, opts) {
|
|
|
|
gnlck._notify(ev, opts);
|
2019-10-20 08:51:19 +00:00
|
|
|
}
|
|
|
|
var certReq = {
|
2019-10-27 10:38:05 +00:00
|
|
|
debug: args.debug || gnlck._defaults.debug,
|
2019-10-20 08:51:19 +00:00
|
|
|
|
2019-10-27 10:38:05 +00:00
|
|
|
challenges: chs,
|
|
|
|
account: acc, // only used if accounts.key.kid exists
|
|
|
|
accountKey:
|
|
|
|
acc.keypair.privateKeyJwk || acc.keypair.private,
|
|
|
|
keypair: acc.keypair, // TODO
|
2019-10-20 08:51:19 +00:00
|
|
|
csr: csr,
|
|
|
|
domains: domains, // because ACME.js v3 uses `domains` still, actually
|
|
|
|
onChallengeStatus: notify,
|
|
|
|
notify: notify // TODO
|
|
|
|
|
|
|
|
// TODO handle this in acme-v2
|
|
|
|
//subject: args.subject,
|
|
|
|
//altnames: args.altnames.slice(0),
|
|
|
|
};
|
|
|
|
return acme.certificates
|
|
|
|
.create(certReq)
|
|
|
|
.then(U._attachCertInfo);
|
|
|
|
})
|
|
|
|
.then(function(pems) {
|
|
|
|
if (kresult.exists) {
|
|
|
|
return pems;
|
|
|
|
}
|
2019-10-20 09:17:19 +00:00
|
|
|
query.keypair = serverKeypair;
|
2019-10-20 08:51:19 +00:00
|
|
|
return db.setKeypair(query, serverKeypair).then(function() {
|
|
|
|
return pems;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.then(function(pems) {
|
|
|
|
// TODO put this in the docs
|
|
|
|
// { cert, chain, privkey, subject, altnames, issuedAt, expiresAt }
|
|
|
|
// Note: the query has been updated
|
|
|
|
query.pems = pems;
|
|
|
|
return db.set(query);
|
|
|
|
})
|
|
|
|
.then(function() {
|
2019-10-27 10:38:05 +00:00
|
|
|
return C._check(gnlck, mconf, db, args);
|
2019-10-20 08:51:19 +00:00
|
|
|
})
|
|
|
|
.then(function(bundle) {
|
|
|
|
// TODO notify Manager
|
|
|
|
delete rawPending[id];
|
|
|
|
return bundle;
|
|
|
|
})
|
|
|
|
.catch(function(err) {
|
|
|
|
// Todo notify manager
|
|
|
|
delete rawPending[id];
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
|
|
|
|
return rawPending[id];
|
|
|
|
};
|
|
|
|
|
|
|
|
// returns pems, if they exist
|
2019-10-27 10:38:05 +00:00
|
|
|
C._check = function(gnlck, mconf, db, args) {
|
2019-10-20 08:51:19 +00:00
|
|
|
var query = {
|
|
|
|
subject: args.subject,
|
|
|
|
// may contain certificate.id
|
2019-10-27 10:38:05 +00:00
|
|
|
certificate: args.certificate,
|
|
|
|
directoryUrl:
|
|
|
|
args.directoryUrl ||
|
|
|
|
mconf.directoryUrl ||
|
|
|
|
gnlck._defaults.directoryUrl
|
2019-10-20 08:51:19 +00:00
|
|
|
};
|
|
|
|
return db.check(query).then(function(pems) {
|
|
|
|
if (!pems) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
pems = U._attachCertInfo(pems);
|
|
|
|
|
|
|
|
// For eager management
|
|
|
|
if (args.subject && !U._certHasDomain(pems, args.subject)) {
|
|
|
|
// TODO report error, but continue the process as with no cert
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// For lazy SNI requests
|
|
|
|
if (args.domain && !U._certHasDomain(pems, args.domain)) {
|
|
|
|
// TODO report error, but continue the process as with no cert
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return U._getKeypair(db, args.subject, query)
|
|
|
|
.then(function(keypair) {
|
2019-10-27 10:38:05 +00:00
|
|
|
return Keypairs.export({
|
|
|
|
jwk: keypair.privateKeyJwk || keypair.private,
|
|
|
|
encoding: 'pem'
|
|
|
|
}).then(function(pem) {
|
|
|
|
pems.privkey = pem;
|
|
|
|
return pems;
|
|
|
|
});
|
2019-10-20 08:51:19 +00:00
|
|
|
})
|
|
|
|
.catch(function() {
|
|
|
|
// TODO report error, but continue the process as with no cert
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// Certificates
|
2019-10-27 10:38:05 +00:00
|
|
|
C._isStale = function(gnlck, mconf, args, pems) {
|
2019-10-20 08:51:19 +00:00
|
|
|
if (args.duplicate) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-10-27 10:38:05 +00:00
|
|
|
var renewAt = C._renewableAt(gnlck, mconf, args, pems);
|
2019-10-20 08:51:19 +00:00
|
|
|
|
|
|
|
if (Date.now() >= renewAt) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2019-10-29 06:16:51 +00:00
|
|
|
C._renewWithStagger = function(gnlck, mconf, args, pems) {
|
|
|
|
var renewOffset = C._renewOffset(gnlck, mconf, args, pems);
|
|
|
|
var renewStagger;
|
|
|
|
try {
|
|
|
|
renewStagger = U._parseDuration(
|
|
|
|
args.renewStagger ||
|
|
|
|
mconf.renewStagger ||
|
|
|
|
gnlck._defaults.renewStagger ||
|
|
|
|
0
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
renewStagger = U._parseDuration(gnlck._defaults.renewStagger);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO check this beforehand
|
|
|
|
if (!args.force && renewStagger / renewOffset >= 0.5) {
|
|
|
|
renewStagger = renewOffset * 0.1;
|
2019-10-20 08:51:19 +00:00
|
|
|
}
|
|
|
|
|
2019-10-29 06:16:51 +00:00
|
|
|
if (renewOffset > 0) {
|
|
|
|
// stagger forward, away from issued at
|
|
|
|
return Math.round(
|
|
|
|
pems.issuedAt + renewOffset + Math.random() * renewStagger
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// stagger backward, toward issued at
|
|
|
|
return Math.round(
|
|
|
|
pems.expiresAt + renewOffset - Math.random() * renewStagger
|
|
|
|
);
|
|
|
|
};
|
|
|
|
C._renewOffset = function(gnlck, mconf, args, pems) {
|
|
|
|
var renewOffset = U._parseDuration(
|
2019-10-27 10:38:05 +00:00
|
|
|
args.renewOffset ||
|
2019-10-29 06:16:51 +00:00
|
|
|
mconf.renewOffset ||
|
|
|
|
gnlck._defaults.renewOffset ||
|
|
|
|
0
|
|
|
|
);
|
2019-10-20 08:51:19 +00:00
|
|
|
var week = 1000 * 60 * 60 * 24 * 6;
|
|
|
|
if (!args.force && Math.abs(renewOffset) < week) {
|
|
|
|
throw new Error(
|
|
|
|
'developer error: `renewOffset` should always be at least a week, use `force` to not safety-check renewOffset'
|
|
|
|
);
|
|
|
|
}
|
2019-10-29 06:16:51 +00:00
|
|
|
return renewOffset;
|
|
|
|
};
|
|
|
|
C._renewableAt = function(gnlck, mconf, args, pems) {
|
|
|
|
if (args.renewAt) {
|
|
|
|
return args.renewAt;
|
|
|
|
}
|
|
|
|
|
|
|
|
var renewOffset = C._renewOffset(gnlck, mconf, args, pems);
|
2019-10-20 08:51:19 +00:00
|
|
|
|
|
|
|
if (renewOffset > 0) {
|
|
|
|
return pems.issuedAt + renewOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
return pems.expiresAt + renewOffset;
|
|
|
|
};
|