certificates.js 9.72 KB
"use strict";

var certificates = module.exports;
var store = certificates;
var U = require("./utils.js");

var fs = require("fs");
var path = require("path");
var PromiseA = require("./promise.js");
var sfs = require("safe-replace");
var readFileAsync = PromiseA.promisify(fs.readFile);
var writeFileAsync = PromiseA.promisify(fs.writeFile);
var mkdirpAsync = PromiseA.promisify(require("@root/mkdirp"));

// Certificates.check
//
// Use certificate.id, or subject, if id hasn't been set, to find a certificate.
// Return an object with string PEMs for cert and chain (or null, not undefined)
certificates.check = function(opts) {
    // { directoryUrl, subject, certificate.id, ... }
    var id = (opts.certificate && opts.certificate.id) || opts.subject;
    //console.log('certificates.check for', opts);

    // For advanced use cases:
    // This just goes to show that any options set in approveDomains() will be available here
    // (the same is true for all of the hooks in this file)
    if (opts.exampleThrowError) {
        return Promise.reject(new Error("You want an error? You got it!"));
    }
    if (opts.exampleReturnNull) {
        return Promise.resolve(null);
    }
    if (opts.exampleReturnCerts) {
        return Promise.resolve(opts.exampleReturnCerts);
    }

    return Promise.all([
        readFileAsync(U._tameWild(privkeyPath(store, opts), id), "ascii"), // 0 // all other PEM types are just
        readFileAsync(U._tameWild(certPath(store, opts), id), "ascii"), // 1 // some arrangement of these 3
        readFileAsync(U._tameWild(chainPath(store, opts), id), "ascii") // 2 // (bundle, combined, fullchain, etc)
    ])
        .then(function(all) {
            ////////////////////////
            // PAY ATTENTION HERE //
            ////////////////////////
            // This is all you have to return: cert, chain
            return {
                cert: all[1], // string PEM. the bare cert, half of the concatonated fullchain.pem you need
                chain: all[2], // string PEM. the bare chain, the second half of the fullchain.pem
                privkey: all[0] // string PEM. optional, allows checkKeypair to be skipped

                // These can be useful to store in your database,
                // but otherwise they're easy to derive from the cert.
                // (when not available they'll be generated from cert-info)
                //, subject: certinfo.subject     // string domain name
                //, altnames: certinfo.altnames   // array of domain name strings
                //, issuedAt: certinfo.issuedAt   // number in ms (a.k.a. NotBefore)
                //, expiresAt: certinfo.expiresAt // number in ms (a.k.a. NotAfter)
            };
        })
        .catch(function(err) {
            // Treat non-exceptional failures as null returns (not undefined)
            if ("ENOENT" === err.code) {
                return null;
            }
            throw err; // True exceptions should be thrown
        });
};

// Certificates.checkKeypair
//
// Use certificate.kid, certificate.id, or subject to find a certificate keypair
// Return an object with string privateKeyPem and/or object privateKeyJwk (or null, not undefined)
certificates.checkKeypair = function(opts) {
    //console.log('certificates.checkKeypair:', opts);

    return readFileAsync(
        U._tameWild(privkeyPath(store, opts), opts.subject),
        "ascii"
    )
        .then(function(key) {
            ////////////////////////
            // PAY ATTENTION HERE //
            ////////////////////////
            return {
                privateKeyPem: key // In this case we only saved privateKeyPem, so we only return it
                //privateKeyJwk: null     // (but it's fine, just different encodings of the same thing)
            };
        })
        .catch(function(err) {
            if ("ENOENT" === err.code) {
                return null;
            }
            throw err;
        });
};

// Certificates.setKeypair({ certificate, subject, keypair, ... }):
//
// Use certificate.kid (or certificate.id or subject if no kid is present) to find a certificate keypair
// Return null (not undefined) on success, or throw on error
certificates.setKeypair = function(opts) {
    var keypair = opts.keypair || keypair;

    // Ignore.
    // Just specific implementation details.
    return mkdirpAsync(
        U._tameWild(path.dirname(privkeyPath(store, opts)), opts.subject)
    ).then(function() {
        // keypair is normally an opaque object, but here it's a PEM for the FS (for things like Apache and Nginx)
        return writeFileAsync(
            U._tameWild(privkeyPath(store, opts), opts.subject),
            keypair.privateKeyPem,
            "ascii"
        ).then(function() {
            return null;
        });
    });
};

// Certificates.set({ subject, pems, ... }):
//
// Use certificate.id (or subject if no ki is present) to save a certificate
// Return null (not undefined) on success, or throw on error
certificates.set = function(opts) {
    //console.log('certificates.set:', opts);
    var pems = {
        cert: opts.pems.cert, // string PEM the first half of the concatonated fullchain.pem cert
        chain: opts.pems.chain, // string PEM the second half (yes, you need this too)
        privkey: opts.pems.privkey // Ignore. string PEM, useful if you have to create bundle.pem
    };

    // Ignore
    // Just implementation specific details (writing lots of combinatons of files)
    return mkdirpAsync(path.dirname(certPath(store, opts)))
        .then(function() {
            return mkdirpAsync(
                path.dirname(U._tameWild(chainPath(store, opts), opts.subject))
            ).then(function() {
                return mkdirpAsync(
                    path.dirname(
                        U._tameWild(fullchainPath(store, opts), opts.subject)
                    )
                ).then(function() {
                    return mkdirpAsync(
                        path.dirname(
                            U._tameWild(bundlePath(store, opts), opts.subject)
                        )
                    ).then(function() {
                        var fullchainPem = [
                            pems.cert.trim() + "\n",
                            pems.chain.trim() + "\n"
                        ].join("\n"); // for Apache, Nginx, etc
                        var bundlePem = [
                            pems.privkey,
                            pems.cert,
                            pems.chain
                        ].join("\n"); // for HAProxy
                        return PromiseA.all([
                            sfs.writeFileAsync(
                                U._tameWild(
                                    certPath(store, opts),
                                    opts.subject
                                ),
                                pems.cert,
                                "ascii"
                            ),
                            sfs.writeFileAsync(
                                U._tameWild(
                                    chainPath(store, opts),
                                    opts.subject
                                ),
                                pems.chain,
                                "ascii"
                            ),
                            // Most web servers need these two
                            sfs.writeFileAsync(
                                U._tameWild(
                                    fullchainPath(store, opts),
                                    opts.subject
                                ),
                                fullchainPem,
                                "ascii"
                            ),
                            // HAProxy needs "bundle.pem" aka "combined.pem"
                            sfs.writeFileAsync(
                                U._tameWild(
                                    bundlePath(store, opts),
                                    opts.subject
                                ),
                                bundlePem,
                                "ascii"
                            )
                        ]);
                    });
                });
            });
        })
        .then(function() {
            // That's your job: return null
            return null;
        });
};

function liveDir(store, opts) {
    return opts.liveDir || path.join(opts.configDir, "live", opts.subject);
}

function privkeyPath(store, opts) {
    var dir = U._tpl(
        store,
        opts,
        opts.serverKeyPath ||
            opts.privkeyPath ||
            opts.domainKeyPath ||
            store.options.serverKeyPath ||
            store.options.privkeyPath ||
            store.options.domainKeyPath ||
            path.join(liveDir(), "privkey.pem")
    );
    return U._tameWild(dir, opts.subject || "");
}

function certPath(store, opts) {
    var pathname =
        opts.certPath ||
        store.options.certPath ||
        path.join(liveDir(), "cert.pem");

    var dir = U._tpl(store, opts, pathname);
    return U._tameWild(dir, opts.subject || "");
}

function fullchainPath(store, opts) {
    var dir = U._tpl(
        store,
        opts,
        opts.fullchainPath ||
            store.options.fullchainPath ||
            path.join(liveDir(), "fullchain.pem")
    );
    return U._tameWild(dir, opts.subject || "");
}

function chainPath(store, opts) {
    var dir = U._tpl(
        store,
        opts,
        opts.chainPath ||
            store.options.chainPath ||
            path.join(liveDir(), "chain.pem")
    );
    return U._tameWild(dir, opts.subject || "");
}

function bundlePath(store, opts) {
    var dir = U._tpl(
        store,
        opts,
        opts.bundlePath ||
            store.options.bundlePath ||
            path.join(liveDir(), "bundle.pem")
    );
    return U._tameWild(dir, opts.subject || "");
}