BREAKING CHANGE(core): streamline scope to certificate retrieval using dns challenge

This commit is contained in:
2020-02-10 20:13:06 +00:00
parent c2036bba90
commit 59173b3ca8
10 changed files with 116 additions and 234 deletions

View File

@ -1,3 +1 @@
export * from './smartacme.classes.smartacme';
export * from './smartacme.classes.certremoteclient';

View File

@ -1,6 +1,6 @@
export type TCertStatus = 'existing' | 'nonexisting' | 'pending' | 'failed';
export interface ICert {
export interface IOldCert {
id: string;
domainName: string;
created: number;

View File

@ -9,7 +9,7 @@ import { Collection, svDb, unI } from '@pushrocks/smartdata';
@plugins.smartdata.Collection(() => {
return CertManager.activeDB;
})
export class Cert extends plugins.smartdata.SmartDataDbDoc<Cert> implements interfaces.ICert {
export class Cert extends plugins.smartdata.SmartDataDbDoc<Cert> implements plugins.tsclass.network.ICert {
@unI()
public id: string;
@ -28,28 +28,30 @@ export class Cert extends plugins.smartdata.SmartDataDbDoc<Cert> implements inte
@svDb()
public csr: string;
/**
* computed value for when the certificate is still valid
*/
get validUntil(): number {
return (
this.created +
plugins.smarttime.getMilliSecondsFromUnits({
days: 90
})
);
@svDb()
public validUntil: number;
public isStillValid(): boolean {
return this.validUntil >= Date.now();
}
get isStillValid(): boolean {
const shouldBeValitAtLeastUntil =
public shouldBeRenewed(): boolean {
const shouldBeValidAtLeastUntil =
Date.now() +
plugins.smarttime.getMilliSecondsFromUnits({
days: 10
});
return this.validUntil >= shouldBeValitAtLeastUntil;
return this.validUntil >= shouldBeValidAtLeastUntil;
}
constructor(optionsArg: interfaces.ICert) {
public update(certDataArg: plugins.tsclass.network.ICert) {
Object.keys(certDataArg).forEach(key => {
this[key] = certDataArg[key];
});
}
constructor(optionsArg: plugins.tsclass.network.ICert) {
super();
if (optionsArg) {
Object.keys(optionsArg).forEach(key => {

View File

@ -16,7 +16,7 @@ export class CertManager {
private mongoDescriptor: plugins.smartdata.IMongoDescriptor;
public smartdataDb: plugins.smartdata.SmartdataDb;
public pendingMap: plugins.lik.Stringmap;
public interestMap: plugins.lik.InterestMap<string, Cert>;
constructor(
smartAcmeArg: SmartAcme,
@ -34,18 +34,17 @@ export class CertManager {
CertManager.activeDB = this.smartdataDb;
// Pending Map
this.pendingMap = new plugins.lik.Stringmap();
this.interestMap = new plugins.lik.InterestMap((certName) => certName);
}
/**
* retrieves a certificate
* @returns the Cert class or null
* @param domainName the domain Name to retrieve the vcertificate for
* @param certDomainNameArg the domain Name to retrieve the vcertificate for
*/
public async retrieveCertificate(domainName: string): Promise<Cert> {
await this.checkCerts();
public async retrieveCertificate(certDomainNameArg: string): Promise<Cert> {
const existingCertificate: Cert = await Cert.getInstance({
domainName
domainName: certDomainNameArg
});
if (existingCertificate) {
@ -59,44 +58,20 @@ export class CertManager {
* stores the certificate
* @param optionsArg
*/
public async storeCertificate(optionsArg: interfaces.ICert) {
public async storeCertificate(optionsArg: plugins.tsclass.network.ICert) {
const cert = new Cert(optionsArg);
await cert.save();
this.pendingMap.removeString(optionsArg.domainName);
}
public async deleteCertificate(domainNameArg: string) {}
/**
* announce a certificate as being in the process of being retrieved
*/
public async announceCertificate(domainNameArg: string) {
this.pendingMap.addString(domainNameArg);
}
/**
* gets the status of a certificate by certDomain name
* @param certDomainArg
*/
public async getCertificateStatus(certDomainArg: string): Promise<interfaces.TCertStatus> {
const isPending = this.pendingMap.checkString(certDomainArg);
if (isPending) {
return 'pending';
const interest = this.interestMap.findInterest(cert.domainName);
if (interest) {
interest.fullfillInterest(cert);
interest.markLost();
}
// otherwise lets continue
const existingCertificate = await this.retrieveCertificate(certDomainArg);
if (existingCertificate) {
return 'existing';
}
return 'nonexisting';
}
/**
* checks all certs for expiration
*/
private async checkCerts() {
// TODO
public async deleteCertificate(certDomainNameArg: string) {
const cert: Cert = await Cert.getInstance({
domainName: certDomainNameArg
});
await cert.delete();
}
}

View File

@ -1,55 +0,0 @@
import * as plugins from './smartacme.plugins';
import * as interfaces from './interfaces';
// tslint:disable-next-line: max-classes-per-file
export class CertRemoteClient {
private remoteUrl: string;
private secret: string;
private logger: plugins.smartlog.Smartlog;
constructor(optionsArg: {
remoteUrl: string;
secret: string;
logger?: plugins.smartlog.Smartlog;
}) {
this.remoteUrl = optionsArg.remoteUrl;
this.secret = optionsArg.secret;
optionsArg.logger
? (this.logger = optionsArg.logger)
: (this.logger = plugins.smartlog.defaultLogger);
}
/**
*
* @param domainNameArg
*/
public async getCertificateForDomain(domainNameArg: string): Promise<interfaces.ICert> {
let certificate: interfaces.ICert;
const doRequestCycle = async (): Promise<interfaces.ICert> => {
const responseBody: interfaces.ICertRemoteResponse = (
await plugins.smartrequest.postJson(this.remoteUrl, {
requestBody: <interfaces.ICertRemoteRequest>{
domainName: domainNameArg,
secret: this.secret
}
})
).body;
switch (responseBody.status as interfaces.TCertStatus) {
case 'pending':
this.logger.log('info', `request for ${domainNameArg} still pending!`);
await plugins.smartdelay.delayFor(5000);
const finalResponse = await doRequestCycle();
return finalResponse;
case 'existing':
this.logger.log('ok', `got certificate for ${domainNameArg}`);
return responseBody.certificate;
case 'failed':
default:
console.log(`could not retrieve certificate for ${domainNameArg}`);
return null;
}
};
certificate = await doRequestCycle();
return certificate;
}
}

View File

@ -13,8 +13,8 @@ export interface ISmartAcmeOptions {
accountPrivateKey?: string;
accountEmail: string;
mongoDescriptor: plugins.smartdata.IMongoDescriptor;
setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
removeChallenge: (domainName: string) => Promise<any>;
setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
environment: 'production' | 'integration';
logger?: plugins.smartlog.Smartlog;
}
@ -41,53 +41,13 @@ export class SmartAcme {
private privateKey: string;
// challenge fullfillment
private setChallenge: (domainName: string, keyAuthorization: string) => Promise<any>;
private removeChallenge: (domainName: string) => Promise<any>;
private setChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
private removeChallenge: (dnsChallengeArg: plugins.tsclass.network.IDnsChallenge) => Promise<any>;
// certmanager
private certmanager: CertManager;
private certmatcher: CertMatcher;
/**
* the remote handler to hand the request and response to.
*/
public certremoteHandler = async (
req: plugins.smartexpress.Request,
res: plugins.smartexpress.Response
) => {
const requestBody: interfaces.ICertRemoteRequest = req.body;
this.logger.log('ok', `got certificate request for ${requestBody.domainName}`);
const certDomain = this.certmatcher.getCertificateDomainNameByDomainName(
requestBody.domainName
);
this.logger.log('ok', `mapping ${requestBody.domainName} to ${certDomain}`);
let status: interfaces.TCertStatus = await this.certmanager.getCertificateStatus(certDomain);
let response: interfaces.ICertRemoteResponse;
switch (status) {
case 'existing':
this.logger.log('ok', `certificate exists for ${certDomain}. Sending certificate!`);
response = {
status,
certificate: await (
await this.certmanager.retrieveCertificate(certDomain)
).createSavableObject()
};
break;
default:
if (status === 'nonexisting') {
this.getCertificateForDomain(certDomain);
status = 'pending';
}
response = {
status
};
break;
}
res.status(200);
res.send(response);
res.end();
};
constructor(optionsArg: ISmartAcmeOptions) {
this.options = optionsArg;
this.options.logger
@ -144,8 +104,8 @@ export class SmartAcme {
* it runs through the following steps
*
* * look in the database
* * if in the database return it
* * of not in the database announce it
* * if in the database and still valid return it
* * if not in the database announce it
* * then get it from letsencrypt
* * store it
* * remove it from the pending map (which it go onto by announcing it)
@ -154,20 +114,28 @@ export class SmartAcme {
* @param domainArg
*/
public async getCertificateForDomain(domainArg: string): Promise<Cert> {
const certDomain = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomain);
const certDomainName = this.certmatcher.getCertificateDomainNameByDomainName(domainArg);
const retrievedCertificate = await this.certmanager.retrieveCertificate(certDomainName);
if (retrievedCertificate) {
if (!retrievedCertificate && await this.certmanager.interestMap.checkInterest(certDomainName)) {
const existingCertificateInterest = this.certmanager.interestMap.findInterest(certDomainName);
const certificate = existingCertificateInterest.interestFullfilled;
return certificate;
} else if (retrievedCertificate && !retrievedCertificate.shouldBeRenewed()) {
return retrievedCertificate;
} else {
await this.certmanager.announceCertificate(certDomain);
} else if (retrievedCertificate && retrievedCertificate.shouldBeRenewed) {
await retrievedCertificate.delete();
}
// lets make sure others get the same interest
await this.certmanager.interestMap.addInterest(certDomainName);
/* Place new order */
const order = await this.client.createOrder({
identifiers: [
{ type: 'dns', value: certDomain },
{ type: 'dns', value: `*.${certDomain}` }
{ type: 'dns', value: certDomainName },
{ type: 'dns', value: `*.${certDomainName}` }
]
});
@ -176,7 +144,7 @@ export class SmartAcme {
for (const authz of authorizations) {
console.log(authz);
const domainDnsName: string = `_acme-challenge.${authz.identifier.value}`;
const fullHostName: string = `_acme-challenge.${authz.identifier.value}`;
const dnsChallenge: string = authz.challenges.find(challengeArg => {
return challengeArg.type === 'dns-01';
});
@ -185,8 +153,11 @@ export class SmartAcme {
try {
/* Satisfy challenge */
await this.setChallenge(domainDnsName, keyAuthorization);
await this.smartdns.checkUntilAvailable(domainDnsName, 'TXT', keyAuthorization, 100, 5000);
await this.setChallenge({
hostName: fullHostName,
challenge: keyAuthorization
});
await this.smartdns.checkUntilAvailable(fullHostName, 'TXT', keyAuthorization, 100, 5000);
console.log('Cool down an extra 60 second for region availability');
await plugins.smartdelay.delayFor(60000);
@ -201,7 +172,10 @@ export class SmartAcme {
} finally {
/* Clean up challenge response */
try {
await this.removeChallenge(domainDnsName);
await this.removeChallenge({
hostName: fullHostName,
challenge: keyAuthorization
});
} catch (e) {
console.log(e);
}
@ -210,8 +184,8 @@ export class SmartAcme {
/* Finalize order */
const [key, csr] = await plugins.acme.forge.createCsr({
commonName: `*.${certDomain}`,
altNames: [certDomain]
commonName: `*.${certDomainName}`,
altNames: [certDomainName]
});
await this.client.finalizeOrder(order, csr);
@ -221,14 +195,19 @@ export class SmartAcme {
await this.certmanager.storeCertificate({
id: plugins.smartunique.shortId(),
domainName: certDomain,
domainName: certDomainName,
privateKey: key.toString(),
publicKey: cert.toString(),
csr: csr.toString(),
created: Date.now()
created: Date.now(),
validUntil:
Date.now() +
plugins.smarttime.getMilliSecondsFromUnits({
days: 90
})
});
const newCertificate = await this.certmanager.retrieveCertificate(certDomain);
const newCertificate = await this.certmanager.retrieveCertificate(certDomainName);
return newCertificate;
}
}

View File

@ -25,7 +25,14 @@ export {
smarttime
};
// thirs party scope
// @tsclass scope
import * as tsclass from '@tsclass/tsclass';
export {
tsclass
}
// third party scope
import * as acme from 'acme-client';
export { acme };