commit df34cbf6c1083febd5fce8199de0793cb2b872e6 Author: AJ ONeal Date: Thu Mar 15 00:41:00 2018 -0600 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..0196434 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +acme-v2.js +========== + +A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. + +In progress + +* get directory +* get nonce +* create account diff --git a/node.js b/node.js new file mode 100644 index 0000000..f3d1db4 --- /dev/null +++ b/node.js @@ -0,0 +1,187 @@ +/*! + * acme-v2.js + * Copyright(c) 2018 AJ ONeal https://ppl.family + * Apache-2.0 OR MIT (and hence also MPL 2.0) +*/ +'use strict'; + +var defaults = { + productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' +, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' +, acmeChallengePrefix: '/.well-known/acme-challenge/' +, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] +, challengeType: 'http-01' // dns-01 +, keyType: 'rsa' // ecdsa +, keySize: 2048 // 256 +}; + +function create(deps) { + if (!deps) { deps = {}; } + deps.LeCore = {}; + deps.pkg = deps.pkg || require('./package.json'); + deps.os = deps.os || require('os'); + deps.process = deps.process || require('process'); + + var uaDefaults = { + pkg: "Greenlock/" + deps.pkg.version + , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" + , node: " Node.js/" + deps.process.version + , user: '' + }; + //var currentUAProps; + + function getUaString() { + var userAgent = ''; + + //Object.keys(currentUAProps) + Object.keys(uaDefaults).forEach(function (key) { + userAgent += uaDefaults[key]; + //userAgent += currentUAProps[key]; + }); + + return userAgent.trim(); + } + + function getRequest(opts) { + if (!opts) { opts = {}; } + + return deps.request.defaults({ + headers: { + 'User-Agent': opts.userAgent || getUaString() + } + }); + } + + deps.request = deps.request || require('request'); + deps.promisify = deps.promisify || require('util').promisify; + + var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; + var request = deps.promisify(getRequest({})); + + var acme2 = { + getAcmeUrls: function () { + var me = this; + return request({ url: directoryUrl }).then(function (resp) { + me._directoryUrls = JSON.parse(resp.body); + me._tos = me._directoryUrls.meta.termsOfService; + return me._directoryUrls; + }); + } + , getNonce: function () { + var me = this; + return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + return me._nonce; + }); + } + // ACME RFC Section 7.3 Account Creation + /* + { + "protected": base64url({ + "alg": "ES256", + "jwk": {...}, + "nonce": "6S8IqOGY7eL2lsGoTZYifg", + "url": "https://example.com/acme/new-account" + }), + "payload": base64url({ + "termsOfServiceAgreed": true, + "onlyReturnExisting": false, + "contact": [ + "mailto:cert-admin@example.com", + "mailto:admin@example.com" + ] + }), + "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" + } + */ + , registerNewAccount: function () { + var me = this; + var RSA = require('rsa-compat').RSA; + var crypto = require('crypto'); + RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = + function (keypair, payload, nonce) { + var prot = {}; + if (nonce) { + if ('string' === typeof nonce) { + prot.nonce = nonce; + } else { + prot = nonce; + } + } + keypair = RSA._internal.import(keypair); + keypair = RSA._internal.importForge(keypair); + keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); + + // Compute JWS signature + var protectedHeader = ""; + if (Object.keys(prot).length) { + protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url }); + } + var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); + var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); + var raw = protected64 + "." + payload64; + var sha256Buf = crypto.createHash('sha256').update(raw).digest(); + var sig64; + + if (RSA._URSA) { + sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); + } else { + sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); + } + + return { + /* + header: { + alg: "RS256" + , jwk: keypair.publicKeyJwk + } + */ + protected: protected64 + , payload: payload64 + , signature: sig64 + }; + }; + + var options = { + email: 'coolaj86@gmail.com' + , keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) + }; + var body = { + termsOfServiceAgreed: true + , onlyReturnExisting: false + , contact: [ 'mailto:' + options.email ] + }; + var payload = JSON.stringify(body, null, 2); + var jws = RSA.signJws( + options.keypair + , new Buffer(payload) + , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } + ); + + console.log('jws:'); + console.log(jws); + return request({ + method: 'POST' + , url: me._directoryUrls.newAccount + , headers: { 'Content-Type': 'application/jose+json' } + , json: jws + }).then(function (resp) { + me._nonce = resp.toJSON().headers['replay-nonce']; + console.log(resp.toJSON()); + return resp.body; + }); + } + }; + return acme2; +} + +var acme2 = create(); +acme2.getAcmeUrls().then(function (body) { + console.log(body); + acme2.getNonce().then(function (nonce) { + console.log(nonce); + acme2.registerNewAccount().then(function (account) { + console.log(account); + }); + }); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..477caca --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "acme-v2", + "version": "1.0.0", + "description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", + "main": "node.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" + }, + "keywords": [ + "acmev2", + "acme-v2", + "acme", + "letsencrypt-v2", + "letsencryptv2", + "greenlock", + "greenlock2" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "request": "^2.85.0", + "rsa-compat": "^1.2.7" + } +}