diff --git a/exec/letsyncrypt.js b/exec/letsyncrypt.js new file mode 100644 index 0000000000000000000000000000000000000000..018c2ea70231d884fcee7eb41c222d457c9c14d5 --- /dev/null +++ b/exec/letsyncrypt.js @@ -0,0 +1,187 @@ +require("acmev2.js", "ACMEv2"); + +/* + * File names used... + */ +var ks_fname = backslash(system.ctrl_dir)+"letsyncrypt.key"; +var setting_fname = backslash(system.ctrl_dir)+"letsyncrypt.ini"; +var sks_fname = backslash(system.ctrl_dir)+"ssl.cert"; +var sbbsini_fname = backslash(system.ctrl_dir)+"sbbs.ini"; +var maincnf_fname = backslash(system.ctrl_dir)+"main.cnf"; +var recycle_sem = backslash(system.ctrl_dir)+"recycle.web"; + +/* + * Get the Web Root + */ +var sbbsini = new File(sbbsini_fname); +if (!sbbsini.open("r")) + throw("Unable to open "+sbbsini.name); +var webroot = backslash(sbbsini.iniGetValue("Web", "RootDirectory", "../web/root")); +sbbsini.close(); + +/* + * Now read in the system password which must be used to encrypt the + * private key. + * + * TODO: What happens when the system password changes? + */ +var maincnf = new File(maincnf_fname); +if (!maincnf.open("rb", true)) + throw("Unable to open "+maincnf.name); +maincnf.position = 186; // Indeed. +var syspass = maincnf.read(40); +syspass = syspass.replace(/\x00/g,''); +maincnf.close(); + +/* + * Now open/create the keyset and RSA signing key for + * ACME. Note that this key is not the one used for the + * final certificate. + */ +var opts = CryptKeyset.KEYOPT.NONE; +if (!file_exists(ks_fname)) + opts = CryptKeyset.KEYOPT.CREATE; +var ks = new CryptKeyset(ks_fname, opts); + +/* + * The ACME key uses "ACMEv2" as the label. + * + * TODO: Regenerate keys etc. + */ +var rsa; +try { + rsa = ks.get_private_key("ACMEv2", syspass); +} +catch(e) { + rsa = new CryptContext(CryptContext.ALGO.RSA); + rsa.keysize=2048/8; + rsa.label="ACMEv2"; + rsa.generate_key(); + ks.add_private_key(rsa, syspass); +} + +/* + * We store the key ID in our ini file so we don't need an extra + * round-trip each session to discover it. + */ +var settings = new File(setting_fname); +settings.open(settings.exists ? "r+" : "w+"); +var key_id = settings.iniGetValue("key_id", system.inet_addr, undefined); +var acme = new ACMEv2({key:rsa, key_id:key_id}); +if (acme.key_id === undefined) { + acme.create_new_account({termsOfServiceAgreed:true,contact:["mailto:sysop@"+system.inet_addr]}); +} +/* + * After the ACMEv2 object is created, we will always have a key_id + * Write it to our ini if it wasn't there already. + */ +if (key_id === undefined) { + settings.iniSetValue("key_id", system.inet_addr, acme.key_id); + key_id = acme.key_id; +} +settings.close(); + +/* + * Create the order, using system.inet_addr + * + * TODO: SNAs... or something. + */ +var order = acme.create_new_order({identifiers:[{type:"dns",value:system.inet_addr}]}); +var authz; +var challenge; +var auth; + +/* + * Find an http-01 authorization + */ +for (auth in order.authorizations) { + authz = acme.get_authorization(order.authorizations[auth]); + for (challenge in authz.challenges) { + if (authz.challenges[challenge].type=='http-01') + break; + } + if (authz.challenges[challenge].type=='http-01') + break; +} +if (authz.challenges[challenge].type!='http-01') + throw("No supported challenges!"); +/* + * Create a place to store the challenge and store it there + * + * TODO: Clean up stale files + */ +mkpath(webroot+".well-known/acme-challenge"); +var token = new File(backslash(webroot+".well-known/acme-challenge")+authz.challenges[challenge].token); +token.open("w"); +token.write(authz.challenges[challenge].token+"."+acme.thumbprint()); +token.close(); + +/* + * Tell the ACMEv2 server we've created the file. + */ +var tmp = acme.accept_challenge(authz.challenges[challenge]); + +/* + * Wait for server to confirm + */ +while (!acme.poll_authorization(order.authorizations[auth])) + mswait(1000); + +/* + * Create CSR + * + * TODO: SANs, virtual hosts, etc... + */ +var csr = new CryptCert(CryptCert.TYPE.CERTREQUEST); +// TODO: Read these from INI file? +csr.oganizationname=system.name; +csr.commonname=system.inet_addr; + +/* + * Now open/create the keyset and RSA signing key for + * Synchronet. + * + * TODO: Regenerate keys etc. + */ +var opts = CryptKeyset.KEYOPT.NONE; +if (!file_exists(sks_fname)) + opts = CryptKeyset.KEYOPT.CREATE; +var sks = new CryptKeyset(sks_fname, opts); + +var certrsa; +try { + certrsa = sks.get_private_key("ssl_cert", syspass); +} +catch(e) { + certrsa = new CryptContext(CryptContext.ALGO.RSA); + certrsa.keysize=2048/8; + certrsa.label="ssl_cert"; + certrsa.generate_key(); + sks.add_private_key(certrsa, syspass); +} +csr.subjectpublickeyinfo=certrsa; +csr.sign(certrsa); +csr.check(); +var csrenc=csr.export(CryptCert.FORMAT.TEXT_CERTIFICATE); +order = acme.finalize_order(order, csr); + +while (order.status !== 'valid') { + order = acme.poll_order(order); +} + +var cert = acme.get_cert(order); +cert.label = "ssl_cert"; + +/* + * Delete the old certificate + */ +try { + sks.delete_key(ssl_cert); +} +catch(e) {} +sks.add_public_key(cert); + +/* + * Recycle webserver + */ +file_touch(recycle_sem);