mirror of https://github.com/certd/certd
				
				
				
			
		
			
				
	
	
		
			177 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			177 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
/**
 | 
						|
 * Example using tls-alpn-01 challenge to generate certificates on-demand
 | 
						|
 */
 | 
						|
 | 
						|
const fs = require('fs');
 | 
						|
const path = require('path');
 | 
						|
const https = require('https');
 | 
						|
const tls = require('tls');
 | 
						|
const acme = require('./../../');
 | 
						|
 | 
						|
const HTTPS_SERVER_PORT = 4443;
 | 
						|
const ALPN_RESPONDER_PORT = 4444;
 | 
						|
const VALID_DOMAINS = ['example.com', 'example.org'];
 | 
						|
const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key'));
 | 
						|
const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt'));
 | 
						|
 | 
						|
const pendingDomains = {};
 | 
						|
const alpnResponses = {};
 | 
						|
const certificateStore = {};
 | 
						|
 | 
						|
function log(m) {
 | 
						|
    process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * On-demand certificate generation using tls-alpn-01
 | 
						|
 */
 | 
						|
 | 
						|
async function getCertOnDemand(client, servername, attempt = 0) {
 | 
						|
    /* Invalid domain */
 | 
						|
    if (!VALID_DOMAINS.includes(servername)) {
 | 
						|
        throw new Error(`Invalid domain: ${servername}`);
 | 
						|
    }
 | 
						|
 | 
						|
    /* Certificate exists */
 | 
						|
    if (servername in certificateStore) {
 | 
						|
        return certificateStore[servername];
 | 
						|
    }
 | 
						|
 | 
						|
    /* Waiting on certificate order to go through */
 | 
						|
    if (servername in pendingDomains) {
 | 
						|
        if (attempt >= 10) {
 | 
						|
            throw new Error(`Gave up waiting on certificate for ${servername}`);
 | 
						|
        }
 | 
						|
 | 
						|
        await new Promise((resolve) => { setTimeout(resolve, 1000); });
 | 
						|
        return getCertOnDemand(client, servername, (attempt + 1));
 | 
						|
    }
 | 
						|
 | 
						|
    /* Create CSR */
 | 
						|
    log(`Creating CSR for ${servername}`);
 | 
						|
    const [key, csr] = await acme.crypto.createCsr({
 | 
						|
        altNames: [servername],
 | 
						|
    });
 | 
						|
 | 
						|
    /* Order certificate */
 | 
						|
    log(`Ordering certificate for ${servername}`);
 | 
						|
    const cert = await client.auto({
 | 
						|
        csr,
 | 
						|
        email: 'test@example.com',
 | 
						|
        termsOfServiceAgreed: true,
 | 
						|
        challengePriority: ['tls-alpn-01'],
 | 
						|
        challengeCreateFn: async (authz, challenge, keyAuthorization) => {
 | 
						|
            alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
 | 
						|
        },
 | 
						|
        challengeRemoveFn: (authz) => {
 | 
						|
            delete alpnResponses[authz.identifier.value];
 | 
						|
        },
 | 
						|
    });
 | 
						|
 | 
						|
    /* Done, store certificate */
 | 
						|
    log(`Certificate for ${servername} created successfully`);
 | 
						|
    certificateStore[servername] = [key, cert];
 | 
						|
    delete pendingDomains[servername];
 | 
						|
    return certificateStore[servername];
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Main
 | 
						|
 */
 | 
						|
 | 
						|
(async () => {
 | 
						|
    try {
 | 
						|
        /**
 | 
						|
         * Initialize ACME client
 | 
						|
         */
 | 
						|
 | 
						|
        log('Initializing ACME client');
 | 
						|
        const client = new acme.Client({
 | 
						|
            directoryUrl: acme.directory.letsencrypt.staging,
 | 
						|
            accountKey: await acme.crypto.createPrivateKey(),
 | 
						|
        });
 | 
						|
 | 
						|
        /**
 | 
						|
         * ALPN responder
 | 
						|
         */
 | 
						|
 | 
						|
        const alpnResponder = https.createServer({
 | 
						|
            /* Fallback cert */
 | 
						|
            key: FALLBACK_KEY,
 | 
						|
            cert: FALLBACK_CERT,
 | 
						|
 | 
						|
            /* Allow acme-tls/1 ALPN protocol */
 | 
						|
            ALPNProtocols: ['acme-tls/1'],
 | 
						|
 | 
						|
            /* Serve ALPN certificate based on servername */
 | 
						|
            SNICallback: async (servername, cb) => {
 | 
						|
                try {
 | 
						|
                    log(`Handling ALPN SNI request for ${servername}`);
 | 
						|
                    if (!Object.keys(alpnResponses).includes(servername)) {
 | 
						|
                        throw new Error(`No ALPN certificate found for ${servername}`);
 | 
						|
                    }
 | 
						|
 | 
						|
                    /* Serve ALPN challenge response */
 | 
						|
                    log(`Found ALPN certificate for ${servername}, serving secure context`);
 | 
						|
                    cb(null, tls.createSecureContext({
 | 
						|
                        key: alpnResponses[servername][0],
 | 
						|
                        cert: alpnResponses[servername][1],
 | 
						|
                    }));
 | 
						|
                }
 | 
						|
                catch (e) {
 | 
						|
                    log(`[ERROR] ${e.message}`);
 | 
						|
                    cb(e.message);
 | 
						|
                }
 | 
						|
            },
 | 
						|
        });
 | 
						|
 | 
						|
        /* Terminate once TLS handshake has been established */
 | 
						|
        alpnResponder.on('secureConnection', (socket) => {
 | 
						|
            socket.end();
 | 
						|
        });
 | 
						|
 | 
						|
        alpnResponder.listen(ALPN_RESPONDER_PORT, () => {
 | 
						|
            log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
 | 
						|
        });
 | 
						|
 | 
						|
        /**
 | 
						|
         * HTTPS server
 | 
						|
         */
 | 
						|
 | 
						|
        const requestListener = (req, res) => {
 | 
						|
            log(`HTTP 200 ${req.headers.host}${req.url}`);
 | 
						|
            res.writeHead(200);
 | 
						|
            res.end('Hello world\n');
 | 
						|
        };
 | 
						|
 | 
						|
        const httpsServer = https.createServer({
 | 
						|
            /* Fallback cert */
 | 
						|
            key: FALLBACK_KEY,
 | 
						|
            cert: FALLBACK_CERT,
 | 
						|
 | 
						|
            /* Serve certificate based on servername */
 | 
						|
            SNICallback: async (servername, cb) => {
 | 
						|
                try {
 | 
						|
                    log(`Handling SNI request for ${servername}`);
 | 
						|
                    const [key, cert] = await getCertOnDemand(client, servername);
 | 
						|
 | 
						|
                    log(`Found certificate for ${servername}, serving secure context`);
 | 
						|
                    cb(null, tls.createSecureContext({ key, cert }));
 | 
						|
                }
 | 
						|
                catch (e) {
 | 
						|
                    log(`[ERROR] ${e.message}`);
 | 
						|
                    cb(e.message);
 | 
						|
                }
 | 
						|
            },
 | 
						|
        }, requestListener);
 | 
						|
 | 
						|
        httpsServer.listen(HTTPS_SERVER_PORT, () => {
 | 
						|
            log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
 | 
						|
        });
 | 
						|
    }
 | 
						|
    catch (e) {
 | 
						|
        log(`[FATAL] ${e.message}`);
 | 
						|
        process.exit(1);
 | 
						|
    }
 | 
						|
})();
 |