diff --git a/README.md b/README.md index 0d3af45..0092090 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,35 @@ -# Bluecrypt™ [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) | A [Root](https://rootprojects.org/acme/) project +# [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) -Free SSL Certificates from Let's Encrypt, right in your Web Browser +Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers Lightweight. Fast. Modern Crypto. Zero dependecies. -(a port of [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js) to the browser) - # Features | 15k gzipped | 55k minified | 88k (2,500 loc) source with comments | -* [x] Let's Encrypt - * [x] ACME draft 15 (supports POST-as-GET) - * [x] Secure support for EC and RSA for account and server keys - * [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations -* [x] VanillaJS, Zero Dependencies +- [x] Let's Encrypt v2.1+ (November 2019) + - [x] ACME draft 15 (supports POST-as-GET) + - [x] Secure support for EC and RSA for account and server keys + - [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations +- [x] Supports International Domain Names (i.e. `.中国`) +- [x] VanillaJS, Zero External Dependencies + - [x] Node.js + - [x] WebPack + +# Want Quick and Easy? + +ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers. + +If you're looking for maximum convenience, try +[Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js). + +- # Online Demos -* Greenlock for the Web -* Bluecrypt ACME Demo +- Greenlock for the Web +- ACME.js Demo We expect that our hosted versions will meet all of yours needs. If they don't, please open an issue to let us know why. @@ -29,34 +39,59 @@ However, in keeping to our values we've made the source visible for others to in # QuickStart -Bluecrypt ACME embeds [Keypairs.js](https://git.rootprojects.org/root/bluecrypt-keypairs.js) +To make it easy to generate, encode, and decode keys and certificates, +ACME.js embeds [Keypairs.js](https://git.rootprojects.org/root/bluecrypt-keypairs.js) and [CSR.js](https://git.rootprojects.org/root/bluecrypt-csr.js) -`bluecrypt-acme.js` -```html - +## Node.js + +```js +var ACME = require('@root/acme'); ``` -`bluecrypt-acme.min.js` -```html - +## WebPack + +```js +var ACME = require('@root/acme'); ``` -You can see `index.html` and `app.js` in the repo for full example usage. +## Vanilla JS -### Instantiate Bluecrypt ACME +```js +var ACME = window.ACME; +``` -Although built for Let's Encrypt, Bluecrypt ACME will work with any server +`acme.js` + +```html + +``` + +`acme.min.js` + +```html + +``` + +## Examples + +You can see `tests/index.js`, `examples/index.html`, `examples/app.js` in the repo for full example usage. + +### Instantiate ACME.js + +Although built for Let's Encrypt, ACME.js will work with any server that supports draft-15 of the ACME spec (includes POST-as-GET support). The `init()` method takes a _directory url_ and initializes internal state according to its response. ```js var acme = ACME.create({}); -acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function () { - // Ready to use, show page - $('body').hidden = false; -}); +acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then( + function() { + // Ready to use, show page + $('body').hidden = false; + } +); ``` ### Create ACME Account with Let's Encrypt @@ -69,20 +104,26 @@ A public account key must be registered before an SSL certificate can be request var accountPrivateKey; var account; -Keypairs.generate({ kty: 'EC' }).then(function (pair) { - accountPrivateKey = pair.private; +Keypairs.generate({ kty: 'EC' }).then(function(pair) { + accountPrivateKey = pair.private; - return acme.accounts.create({ - agreeToTerms: function (tos) { - if (window.confirm("Do you agree to the Bluecrypt and Let's Encrypt Terms of Service?")) { - return Promise.resolve(tos); - } - } - , accountKeypair: { privateKeyJwk: pair.private } - , email: $('.js-email-input').value - }).then(function (_account) { - account = _account; - }); + return acme.accounts + .create({ + agreeToTerms: function(tos) { + if ( + window.confirm( + "Do you agree to the ACME.js and Let's Encrypt Terms of Service?" + ) + ) { + return Promise.resolve(tos); + } + }, + accountKeypair: { privateKeyJwk: pair.private }, + email: $('.js-email-input').value + }) + .then(function(_account) { + account = _account; + }); }); ``` @@ -97,26 +138,27 @@ is a required part of the process, which requires `set` and `remove` callbacks/p ```js var serverPrivateKey; -Keypairs.generate({ kty: 'EC' }).then(function (pair) { - serverPrivateKey = pair.private; - - return acme.certificates.create({ - agreeToTerms: function (tos) { - return tos; - } - , account: account - , accountKeypair: { privateKeyJwk: accountPrivateKey } - , serverKeypair: { privateKeyJwk: serverPrivateKey } - , domains: ['example.com','www.example.com'] - , challenges: challenges // must be implemented - , skipDryRun: true - }).then(function (results) { - console.log('Got SSL Certificate:'); - console.log(results.expires); - console.log(results.cert); - console.log(results.chain); - }); +Keypairs.generate({ kty: 'EC' }).then(function(pair) { + serverPrivateKey = pair.private; + return acme.certificates + .create({ + agreeToTerms: function(tos) { + return tos; + }, + account: account, + accountKeypair: { privateKeyJwk: accountPrivateKey }, + serverKeypair: { privateKeyJwk: serverPrivateKey }, + domains: ['example.com', 'www.example.com'], + challenges: challenges, // must be implemented + skipDryRun: true + }) + .then(function(results) { + console.log('Got SSL Certificate:'); + console.log(results.expires); + console.log(results.cert); + console.log(results.chain); + }); }); ``` @@ -131,22 +173,42 @@ reserved for server-side plugins. ```js var challenges = { - 'http-01': { - set: function (opts) { - console.info('http-01 set challenge:'); - console.info(opts.challengeUrl); - console.info(opts.keyAuthorization); - while (!window.confirm("Upload the challenge file before continuing.")) {} - return Promise.resolve(); - } - , remove: function (opts) { - console.log('http-01 remove challenge:', opts.challengeUrl); - return Promise.resolve(); - } - } + 'http-01': { + set: function(opts) { + console.info('http-01 set challenge:'); + console.info(opts.challengeUrl); + console.info(opts.keyAuthorization); + while ( + !window.confirm('Upload the challenge file before continuing.') + ) {} + return Promise.resolve(); + }, + remove: function(opts) { + console.log('http-01 remove challenge:', opts.challengeUrl); + return Promise.resolve(); + } + } }; ``` +# IDN - International Domain Names + +Convert domain names to `punycode` before creating the certificate: + +```js +var punycode = require('punycode'); + +acme.certificates.create({ + // ... + domains: ['example.com', 'www.example.com'].map(function(name) { + return punycode.toASCII(name); + }) +}); +``` + +The punycode library itself is lightweight and dependency-free. +It is available both in node and for browsers. + # Full Documentation See [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js). @@ -175,16 +237,16 @@ We also offer consulting for all-things-ACME and Let's Encrypt. # Legal & Rules of the Road -Bluecrypt™ and Greenlock™ are [trademarks](https://rootprojects.org/legal/#trademark) of AJ ONeal +Greenlock™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal The rule of thumb is "attribute, but don't confuse". For example: -> Built with [Bluecrypt ACME](https://git.rootprojects.org/root/bluecrypt-acme.js) (a [Root](https://rootprojects.org) project). +> Built with [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) (a [Root](https://rootprojects.org) project). Please [contact us](mailto:aj@therootcompany.com) if have any questions in regards to our trademark, attribution, and/or visible source policies. We want to build great software and a great community. -[bluecrypt.js](https://git.coolaj86.com/coolaj86/bluecrypt.js) | +[ACME.js](https://git.rootprojects.org/root/acme.js) | MPL-2.0 | [Terms of Use](https://therootcompany.com/legal/#terms) | [Privacy Policy](https://therootcompany.com/legal/#privacy) diff --git a/app.js b/examples/app.js similarity index 100% rename from app.js rename to examples/app.js diff --git a/index.html b/examples/index.html similarity index 100% rename from index.html rename to examples/index.html diff --git a/examples/server.js b/examples/server.js new file mode 100644 index 0000000..89111ca --- /dev/null +++ b/examples/server.js @@ -0,0 +1,174 @@ +'use strict'; + +var crypto = require('crypto'); +//var dnsjs = require('dns-suite'); +var dig = require('dig.js/dns-request'); +var request = require('util').promisify(require('@root/request')); +var express = require('express'); +var app = express(); + +var nameservers = require('dns').getServers(); +var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; +var nameserver = nameservers[index]; + +app.use('/', express.static(__dirname)); +app.use('/api', express.json()); +app.get('/api/dns/:domain', function(req, res, next) { + var domain = req.params.domain; + var casedDomain = domain + .toLowerCase() + .split('') + .map(function(ch) { + // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is + // ch = ch | 0x20; + return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); + }) + .join(''); + var typ = req.query.type; + var query = { + header: { + id: crypto.randomBytes(2).readUInt16BE(0), + qr: 0, + opcode: 0, + aa: 0, // Authoritative-Only + tc: 0, // NA + rd: 1, // Recurse + ra: 0, // NA + rcode: 0 // NA + }, + question: [ + { + name: casedDomain, + //, type: typ || 'A' + typeName: typ || 'A', + className: 'IN' + } + ] + }; + var opts = { + onError: function(err) { + next(err); + }, + onMessage: function(packet) { + var fail0x20; + + if (packet.id !== query.id) { + console.error( + "[SECURITY] ignoring packet for '" + + packet.question[0].name + + "' due to mismatched id" + ); + console.error(packet); + return; + } + + packet.question.forEach(function(q) { + // if (-1 === q.name.lastIndexOf(cli.casedQuery)) + if (q.name !== casedDomain) { + fail0x20 = q.name; + } + }); + + ['question', 'answer', 'authority', 'additional'].forEach(function( + group + ) { + (packet[group] || []).forEach(function(a) { + var an = a.name; + var i = domain + .toLowerCase() + .lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM + var j = a.name + .toLowerCase() + .lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM + + // it's important to note that these should only relpace changes in casing that we expected + // any abnormalities should be left intact to go "huh?" about + // TODO detect abnormalities? + if (-1 !== i) { + // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) + a.name = a.name.replace( + casedDomain.substr(i), + domain.substr(i) + ); + } else if (-1 !== j) { + // "www.example.com".replace("EXamPLE.cOm", "example.com") + a.name = + a.name.substr(0, j) + + a.name.substr(j).replace(casedDomain, domain); + } + + // NOTE: right now this assumes that anything matching the query matches all the way to the end + // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly + // (but I don't think it should need to) + if (a.name.length !== an.length) { + console.error( + "[ERROR] question / answer mismatch: '" + + an + + "' != '" + + a.length + + "'" + ); + console.error(a); + } + }); + }); + + if (fail0x20) { + console.warn( + ";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" + + casedDomain + + "' but got response for '" + + fail0x20 + + "'." + ); + return; + } + + res.send({ + header: packet.header, + question: packet.question, + answer: packet.answer, + authority: packet.authority, + additional: packet.additional, + edns_options: packet.edns_options + }); + }, + onListening: function() {}, + onSent: function(/*res*/) {}, + onTimeout: function(res) { + console.error('dns timeout:', res); + next(new Error('DNS timeout - no response')); + }, + onClose: function() {}, + //, mdns: cli.mdns + nameserver: nameserver, + port: 53, + timeout: 2000 + }; + + dig.resolveJson(query, opts); +}); +app.get('/api/http', function(req, res) { + var url = req.query.url; + return request({ method: 'GET', url: url }).then(function(resp) { + res.send(resp.body); + }); +}); +app.get('/api/_acme_api_', function(req, res) { + res.send({ success: true }); +}); + +module.exports = app; +if (require.main === module) { + // curl -L http://localhost:3000/api/dns/example.com?type=A + console.info('Listening on localhost:3000'); + app.listen(3000); + console.info('Try this:'); + console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); + console.info( + "\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'" + ); + console.info( + "\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'" + ); +} diff --git a/lib/acme.js b/lib/acme.js index 46cec2f..6d7071b 100644 --- a/lib/acme.js +++ b/lib/acme.js @@ -5,10 +5,11 @@ 'use strict'; /* globals Promise */ +require('@root/encoding/bytes'); +var Enc = require('@root/encoding/base64'); var ACME = module.exports; //var Keypairs = exports.Keypairs || {}; //var CSR = exports.CSR; -var Enc = require('omnibuffer'); var sha2 = require('./node/sha2.js'); var http = require('./node/http.js'); @@ -37,21 +38,22 @@ ACME.challengePrefixes = { }; ACME.challengeTests = { 'http-01': function(me, auth) { - return me.http01(auth).then(function(keyAuth) { + var ch = auth.challenge; + return me.http01(ch).then(function(keyAuth) { var err; // TODO limit the number of bytes that are allowed to be downloaded - if (auth.keyAuthorization === (keyAuth || '').trim()) { + if (ch.keyAuthorization === (keyAuth || '').trim()) { return true; } err = new Error( 'Error: Failed HTTP-01 Pre-Flight / Dry Run.\n' + "curl '" + - auth.challengeUrl + + ch.challengeUrl + "'\n" + "Expected: '" + - auth.keyAuthorization + + ch.keyAuthorization + "'\n" + "Got: '" + keyAuth + @@ -64,12 +66,13 @@ ACME.challengeTests = { }, 'dns-01': function(me, auth) { // remove leading *. on wildcard domains - return me.dns01(auth).then(function(ans) { + var ch = auth.challenge; + return me.dns01(ch).then(function(ans) { var err; if ( ans.answer.some(function(txt) { - return auth.dnsAuthorization === txt.data[0]; + return ch.dnsAuthorization === txt.data[0]; }) ) { return true; @@ -78,9 +81,9 @@ ACME.challengeTests = { err = new Error( 'Error: Failed DNS-01 Pre-Flight Dry Run.\n' + "dig TXT '" + - auth.dnsHost + + ch.dnsHost + "' does not return '" + - auth.dnsAuthorization + + ch.dnsAuthorization + "'\n" + 'See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4' ); @@ -565,16 +568,21 @@ ACME._challengeToAuth = function( // For backwards compat with the v2.7 plugins auth.challenge = auth; // TODO can we use just { challenge: auth }? - auth.request = function() { - // TODO see https://git.rootprojects.org/root/acme.js/issues/### - console.warn( - "[warn] deprecated use of request on '" + - auth.type + - "' challenge object. Receive from challenger.init() instead." - ); - me.request.apply(null, arguments); + // auth.request = ; + + // TODO get rid of no-challenge backwards compat challenge + return { + challenge: auth, + request: function() { + // TODO see https://git.rootprojects.org/root/acme.js/issues/### + console.warn( + "[warn] deprecated use of request on '" + + auth.type + + "' challenge object. Receive from challenger.init() instead." + ); + me.request.apply(null, arguments); + } }; - return auth; }); }; @@ -592,8 +600,9 @@ ACME._postChallenge = function(me, options, auth) { var MAX_POLL = me.retryPoll || 8; var MAX_PEND = me.retryPending || 4; var count = 0; + var ch = auth.challenge; - var altname = ACME._untame(auth.identifier.value, auth.wildcard); + var altname = ACME._untame(ch.identifier.value, ch.wildcard); /* POST /acme/authz/1234 HTTP/1.1 @@ -619,7 +628,7 @@ ACME._postChallenge = function(me, options, auth) { } return ACME._jwsRequest(me, { options: options, - url: auth.url, + url: ch.url, protected: { kid: options._kid }, payload: Enc.strToBuf(JSON.stringify({ status: 'deactivated' })) }).then(function(resp) { @@ -651,11 +660,11 @@ ACME._postChallenge = function(me, options, auth) { } // TODO POST-as-GET return me - .request({ method: 'GET', url: auth.url, json: true }) + .request({ method: 'GET', url: ch.url, json: true }) .then(function(resp) { if ('processing' === resp.body.status) { if (me.debug) { - console.debug('poll: again', auth.url); + console.debug('poll: again', ch.url); } return ACME._wait(RETRY_INTERVAL).then(pollStatus); } @@ -668,7 +677,7 @@ ACME._postChallenge = function(me, options, auth) { .then(respondToChallenge); } if (me.debug) { - console.debug('poll: again', auth.url); + console.debug('poll: again', ch.url); } return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); } @@ -719,7 +728,7 @@ ACME._postChallenge = function(me, options, auth) { } return ACME._jwsRequest(me, { options: options, - url: auth.url, + url: ch.url, protected: { kid: options._kid }, payload: Enc.strToBuf(JSON.stringify({})) }).then(function(resp) { @@ -736,13 +745,14 @@ ACME._postChallenge = function(me, options, auth) { return respondToChallenge(); }; ACME._setChallenge = function(me, options, auth) { + var ch = auth.challenge; return Promise.resolve().then(function() { var challengers = options.challenges || {}; - var challenger = challengers[auth.type] && challengers[auth.type].set; + var challenger = challengers[ch.type] && challengers[ch.type].set; if (!challenger) { throw new Error( "options.challenges did not have a valid entry for '" + - auth.type + + ch.type + "'" ); } @@ -760,7 +770,7 @@ ACME._setChallenge = function(me, options, auth) { }); } else { throw new Error( - "Bad function signature for '" + auth.type + "' challenge.set()" + "Bad function signature for '" + ch.type + "' challenge.set()" ); } }); @@ -957,6 +967,17 @@ ACME._getCertificate = function(me, options) { ); } + // a cheap check to see if there are non-ascii characters in any of the domains + var nonAsciiDomains = options.domains.some(function(d) { + // IDN / unicode / utf-8 / punycode + return Enc.strToBin(d) !== d; + }); + if (nonAsciiDomains) { + throw new Error( + "please use the 'punycode' module to convert unicode domain names to punycode" + ); + } + // It's just fine if there's no account, we'll go get the key id we need via the existing key options._kid = options._kid || @@ -1034,7 +1055,10 @@ ACME._getCertificate = function(me, options) { return 0; }) .map(function(hostname) { - return { type: 'dns', value: hostname }; + return { + type: 'dns', + value: hostname + }; }) //, "notBefore": "2016-01-01T00:00:00Z" //, "notAfter": "2016-01-08T00:00:00Z" @@ -1164,14 +1188,15 @@ ACME._getCertificate = function(me, options) { return; } - if (!me._canUse[auth.type] || me.skipChallengeTest) { + var ch = auth.challenge; + if (!me._canUse[ch.type] || me.skipChallengeTest) { // not so much "valid" as "not invalid" // but in this case we can't confirm either way validAuths.push(auth); return checkNext(); } - return ACME.challengeTests[auth.type](me, auth) + return ACME.challengeTests[ch.type](me, auth) .then(function() { validAuths.push(auth); }) @@ -1272,10 +1297,7 @@ ACME._generateCsrWeb64 = function(me, options, validatedDomains) { return Promise.resolve(csr); } - return ACME._importKeypair( - me, - options.serverKeypair || options.domainKeypair - ).then(function(pair) { + return ACME._importKeypair(me, options.serverKeypair).then(function(pair) { return me.CSR.csr({ jwk: pair.private, domains: validatedDomains, @@ -1302,8 +1324,8 @@ ACME.create = function create(me) { //me.Keypairs = me.Keypairs || require('keypairs'); //me.request = me.request || require('@root/request'); if (!me.dns01) { - me.dns01 = function(auth) { - return ACME._dns01(me, auth); + me.dns01 = function(ch) { + return ACME._dns01(me, ch); }; } // backwards compat @@ -1311,8 +1333,8 @@ ACME.create = function create(me) { me.dig = me.dns01; } if (!me.http01) { - me.http01 = function(auth) { - return ACME._http01(me, auth); + me.http01 = function(ch) { + return ACME._http01(me, ch); }; } @@ -1505,9 +1527,9 @@ ACME._prnd = function(n) { ACME._toHex = function(pair) { return parseInt(pair, 10).toString(16); }; -ACME._dns01 = function(me, auth) { +ACME._dns01 = function(me, ch) { return new me.request({ - url: me._baseUrl + '/api/dns/' + auth.dnsHost + '?type=TXT' + url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT' }).then(function(resp) { var err; if (!resp.body || !Array.isArray(resp.body.answer)) { @@ -1527,8 +1549,8 @@ ACME._dns01 = function(me, auth) { }; }); }; -ACME._http01 = function(me, auth) { - var url = encodeURIComponent(auth.challengeUrl); +ACME._http01 = function(me, ch) { + var url = encodeURIComponent(ch.challengeUrl); return new me.request({ url: me._baseUrl + '/api/http?url=' + url }).then(function(resp) { @@ -1537,20 +1559,27 @@ ACME._http01 = function(me, auth) { }; ACME._removeChallenge = function(me, options, auth) { var challengers = options.challenges || {}; - var removeChallenge = - challengers[auth.type] && challengers[auth.type].remove; + var ch = auth.challenge; + var removeChallenge = challengers[ch.type] && challengers[ch.type].remove; if (!removeChallenge) { throw new Error('challenge plugin is missing remove()'); } if (1 === removeChallenge.length) { return Promise.resolve(removeChallenge(auth)).then( function() {}, - function() {} + function(e) { + console.error('Error during remove challenge:'); + console.error(e); + } ); } else if (2 === removeChallenge.length) { return new Promise(function(resolve) { removeChallenge(auth, function(err) { resolve(); + if (err) { + console.error('Error during remove challenge:'); + console.error(err); + } return err; }); }); @@ -1585,13 +1614,11 @@ ACME._getZones = function(me, presenter, dnsHosts) { dnsHosts: dnsHosts, request: me.request }; - // back/forwards-compat - challenge.challenge = challenge; return ACME._wrapCb( me, presenter, 'zones', - challenge, + { challenge: challenge }, 'an array of zone names' ); }; diff --git a/lib/asn1/packer.js b/lib/asn1/packer.js index 50e5148..191172d 100644 --- a/lib/asn1/packer.js +++ b/lib/asn1/packer.js @@ -1,7 +1,7 @@ 'use strict'; var ASN1 = module.exports; -var Enc = require('omnibuffer'); +var Enc = require('@root/encoding/hex'); // // Packer diff --git a/lib/asn1/parser.js b/lib/asn1/parser.js index 1eebfca..3c5c961 100644 --- a/lib/asn1/parser.js +++ b/lib/asn1/parser.js @@ -5,7 +5,7 @@ 'use strict'; var ASN1 = module.exports; -var Enc = require('omnibuffer'); +var Enc = require('@root/encoding/hex'); // // Parser diff --git a/lib/csr.js b/lib/csr.js index a75494d..6cb691f 100644 --- a/lib/csr.js +++ b/lib/csr.js @@ -5,12 +5,13 @@ 'use strict'; /*global Promise*/ +var Enc = require('@root/encoding'); + var ASN1 = require('./asn1/packer.js'); // DER, actually var Asn1 = ASN1.Any; var BitStr = ASN1.BitStr; var UInt = ASN1.UInt; var Asn1Parser = require('./asn1/parser.js'); -var Enc = require('omnibuffer'); var PEM = require('./pem.js'); var X509 = require('./x509.js'); var Keypairs = require('./keypairs'); @@ -155,7 +156,8 @@ X509.packCsr = function(asn1pubkey, domains) { Asn1( '30', Asn1('06', '550403'), - Asn1('0c', Enc.utf8ToHex(domains[0])) + // TODO utf8 => punycode + Asn1('0c', Enc.strToHex(domains[0])) ) ) ), @@ -184,7 +186,8 @@ X509.packCsr = function(asn1pubkey, domains) { '30', domains .map(function(d) { - return Asn1('82', Enc.utf8ToHex(d)); + // TODO utf8 => punycode + return Asn1('82', Enc.strToHex(d)); }) .join('') ) @@ -235,7 +238,6 @@ CSR._info = function(der) { } //c.children[1]; // signature type var req = c.children[0]; - // TODO utf8 if (4 !== req.children.length) { throw new Error( "doesn't look like a certificate request: expected 4 parts to request" @@ -243,7 +245,7 @@ CSR._info = function(der) { } // 0 null // 1 commonName / subject - var sub = Enc.bufToBin( + var sub = Enc.bufToStr( req.children[1].children[0].children[0].children[1].value ); // 3 public key (type, key) @@ -305,8 +307,8 @@ CSR._info = function(der) { return seq2.children[1].children[0].children.map(function( name ) { - // TODO utf8 - return Enc.bufToBin(name.value); + // TODO utf8 => punycode + return Enc.bufToStr(name.value); }); })[0]; })[0]; diff --git a/lib/ecdsa.js b/lib/ecdsa.js index 4879937..07f8008 100644 --- a/lib/ecdsa.js +++ b/lib/ecdsa.js @@ -1,6 +1,8 @@ /*global Promise*/ 'use strict'; +var Enc = require('@root/encoding'); + var EC = module.exports; var native = require('./node/ecdsa.js'); @@ -10,7 +12,6 @@ var SSH; var x509 = require('./x509.js'); var PEM = require('./pem.js'); //var SSH = require('./ssh-keys.js'); -var Enc = require('omnibuffer'); var sha2 = require('./node/sha2.js'); // 1.2.840.10045.3.1.7 diff --git a/lib/encoding.js b/lib/encoding.js deleted file mode 100644 index 44cf002..0000000 --- a/lib/encoding.js +++ /dev/null @@ -1,151 +0,0 @@ -(function(exports) { - var Enc = (exports.Enc = {}); - - Enc.bufToBin = function(buf) { - var bin = ''; - // cannot use .map() because Uint8Array would return only 0s - buf.forEach(function(ch) { - bin += String.fromCharCode(ch); - }); - return bin; - }; - - Enc.bufToHex = function toHex(u8) { - var hex = []; - var i, h; - var len = u8.byteLength || u8.length; - - for (i = 0; i < len; i += 1) { - h = u8[i].toString(16); - if (h.length % 2) { - h = '0' + h; - } - hex.push(h); - } - - return hex.join('').toLowerCase(); - }; - - Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { - var r = str % 4; - if (2 === r) { - str += '=='; - } else if (3 === r) { - str += '='; - } - return str.replace(/-/g, '+').replace(/_/g, '/'); - }; - - Enc.base64ToBuf = function(b64) { - return Enc.binToBuf(atob(b64)); - }; - Enc.binToBuf = function(bin) { - var arr = bin.split('').map(function(ch) { - return ch.charCodeAt(0); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; - }; - Enc.bufToHex = function(u8) { - var hex = []; - var i, h; - var len = u8.byteLength || u8.length; - - for (i = 0; i < len; i += 1) { - h = u8[i].toString(16); - if (h.length % 2) { - h = '0' + h; - } - hex.push(h); - } - - return hex.join('').toLowerCase(); - }; - Enc.numToHex = function(d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; - }; - - Enc.bufToUrlBase64 = function(u8) { - return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); - }; - - Enc.base64ToUrlBase64 = function(str) { - return str - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - }; - - Enc.bufToBase64 = function(u8) { - var bin = ''; - u8.forEach(function(i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); - }; - - Enc.hexToBuf = function(hex) { - var arr = []; - hex.match(/.{2}/g).forEach(function(h) { - arr.push(parseInt(h, 16)); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; - }; - - Enc.numToHex = function(d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; - }; - - // - // JWK to SSH (tested working) - // - Enc.base64ToHex = function(b64) { - var bin = atob(Enc.urlBase64ToBase64(b64)); - return Enc.binToHex(bin); - }; - - Enc.binToHex = function(bin) { - return bin - .split('') - .map(function(ch) { - var h = ch.charCodeAt(0).toString(16); - if (h.length % 2) { - h = '0' + h; - } - return h; - }) - .join(''); - }; - // TODO are there any nuance differences here? - Enc.utf8ToHex = Enc.binToHex; - - Enc.hexToBase64 = function(hex) { - return btoa(Enc.hexToBin(hex)); - }; - - Enc.hexToBin = function(hex) { - return hex - .match(/.{2}/g) - .map(function(h) { - return String.fromCharCode(parseInt(h, 16)); - }) - .join(''); - }; - - Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { - var r = str % 4; - if (2 === r) { - str += '=='; - } else if (3 === r) { - str += '='; - } - return str.replace(/-/g, '+').replace(/_/g, '/'); - }; -})('undefined' !== typeof exports ? module.exports : window); diff --git a/lib/keypairs.js b/lib/keypairs.js index 601e662..1b027c7 100644 --- a/lib/keypairs.js +++ b/lib/keypairs.js @@ -1,11 +1,13 @@ /*global Promise*/ 'use strict'; +require('@root/encoding/bytes'); +var Enc = require('@root/encoding/base64'); + var Keypairs = module.exports; var Rasha = require('./rsa.js'); var Eckles = require('./ecdsa.js'); var native = require('./node/keypairs.js'); -var Enc = require('omnibuffer'); Keypairs._stance = "We take the stance that if you're knowledgeable enough to" + @@ -224,7 +226,7 @@ Keypairs.signJws = function(opts) { } // Converting to a buffer, even if it was just converted to a string if ('string' === typeof payload) { - payload = Enc.binToBuf(payload); + payload = Enc.strToBuf(payload); } var protected64 = Enc.strToUrlBase64(protectedHeader); @@ -311,20 +313,3 @@ function setTime(time) { return now + mult * num; } - -Enc.hexToBuf = function(hex) { - var arr = []; - hex.match(/.{2}/g).forEach(function(h) { - arr.push(parseInt(h, 16)); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; -Enc.strToUrlBase64 = function(str) { - return Enc.bufToUrlBase64(Enc.binToBuf(str)); -}; -Enc.binToBuf = function(bin) { - var arr = bin.split('').map(function(ch) { - return ch.charCodeAt(0); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; diff --git a/lib/node/rsa.js b/lib/node/rsa.js index 4be0b77..5d322e1 100644 --- a/lib/node/rsa.js +++ b/lib/node/rsa.js @@ -118,7 +118,7 @@ function toJwks(oldpair) { } // TODO -var Enc = require('omnibuffer'); +var Enc = require('@root/encoding/base64'); x509.parsePkcs1 = function parseRsaPkcs1(buf, asn1, jwk) { if ( !asn1.children.every(function(el) { diff --git a/lib/pem.js b/lib/pem.js index db61fc3..e27248f 100644 --- a/lib/pem.js +++ b/lib/pem.js @@ -1,7 +1,7 @@ 'use strict'; +var Enc = require('@root/encoding/base64'); var PEM = module.exports; -var Enc = require('omnibuffer'); PEM.packBlock = function(opts) { // TODO allow for headers? diff --git a/lib/rsa.js b/lib/rsa.js index 4925564..a86456a 100644 --- a/lib/rsa.js +++ b/lib/rsa.js @@ -7,7 +7,7 @@ var x509 = require('./x509.js'); var PEM = require('./pem.js'); //var SSH = require('./ssh-keys.js'); var sha2 = require('./node/sha2.js'); -var Enc = require('omnibuffer'); +var Enc = require('@root/encoding/base64'); RSA._universal = 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; diff --git a/lib/x509.js b/lib/x509.js index 72e9db1..aba8da3 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -5,7 +5,7 @@ var ASN1 = require('./asn1/packer.js'); var Asn1 = ASN1.Any; var UInt = ASN1.UInt; var BitStr = ASN1.BitStr; -var Enc = require('omnibuffer'); +var Enc = require('@root/encoding'); // 1.2.840.10045.3.1.7 // prime256v1 (ANSI X9.62 named elliptic curve) diff --git a/package-lock.json b/package-lock.json index 5c1f3e7..5807f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,28 +4,17 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@root/encoding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", + "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" + }, "@root/request": { "version": "1.3.10", "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.10.tgz", "integrity": "sha512-GSn8dfsGp0juJyXS9k7B/DjYm7Axe85wiCHfPs30eQ+/V6p2aqey45e1czb3ZwP+iPmzWCKXahhWnZhSDIil6w==", "dev": true }, - "accepts": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.6.tgz", - "integrity": "sha512-QsaoUD2dpVpjENy8JFpQnXP9vyzoZPmAoKrE3S6HtSB7qzSebkJNnmdY4p004FQUSSiHXPueENpoeuUW/7a8Ig==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -38,24 +27,6 @@ "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", "dev": true }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "dev": true, - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -66,12 +37,6 @@ "concat-map": "0.0.1" } }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, "cli": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", @@ -82,63 +47,12 @@ "glob": "^7.1.1" } }, - "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, "dig.js": { "version": "1.3.9", "resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz", @@ -177,101 +91,12 @@ "integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==", "dev": true }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, - "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -297,27 +122,6 @@ "from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4", "dev": true }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -334,51 +138,6 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", - "dev": true - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -388,27 +147,6 @@ "brace-expansion": "^1.1.7" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -418,157 +156,16 @@ "wrappy": "1" } }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", - "dev": true - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "dev": true, - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", - "dev": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, "wrappy": { diff --git a/package.json b/package.json index 5bdd5f5..78e0c96 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dist" ], "scripts": { - "build": "node bin/bundle.js", + "build": "nodex bin/bundle.js", "lint": "jshint lib bin", "test": "node server.js", "start": "node server.js" @@ -41,12 +41,14 @@ ], "author": "AJ ONeal (https://coolaj86.com/)", "license": "MPL-2.0", + "dependencies": { + "@root/encoding": "^1.0.1" + }, "devDependencies": { "@root/request": "^1.3.10", "dig.js": "^1.3.9", "dns-suite": "^1.2.12", "dotenv": "^8.1.0", - "express": "^4.16.4", - "uglify-js": "^3.6.0" + "punycode": "^1.4.1" } } diff --git a/server.js b/server.js deleted file mode 100644 index 5ae0a55..0000000 --- a/server.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -var crypto = require('crypto'); -//var dnsjs = require('dns-suite'); -var dig = require('dig.js/dns-request'); -var request = require('util').promisify(require('@root/request')); -var express = require('express'); -var app = express(); - -var nameservers = require('dns').getServers(); -var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; -var nameserver = nameservers[index]; - -app.use('/', express.static(__dirname)); -app.use('/api', express.json()); -app.get('/api/dns/:domain', function (req, res, next) { - var domain = req.params.domain; - var casedDomain = domain.toLowerCase().split('').map(function (ch) { - // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is - // ch = ch | 0x20; - return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); - }).join(''); - var typ = req.query.type; - var query = { - header: { - id: crypto.randomBytes(2).readUInt16BE(0) - , qr: 0 - , opcode: 0 - , aa: 0 // Authoritative-Only - , tc: 0 // NA - , rd: 1 // Recurse - , ra: 0 // NA - , rcode: 0 // NA - } - , question: [ - { name: casedDomain - //, type: typ || 'A' - , typeName: typ || 'A' - , className: 'IN' - } - ] - }; - var opts = { - onError: function (err) { - next(err); - } - , onMessage: function (packet) { - var fail0x20; - - if (packet.id !== query.id) { - console.error('[SECURITY] ignoring packet for \'' + packet.question[0].name + '\' due to mismatched id'); - console.error(packet); - return; - } - - packet.question.forEach(function (q) { - // if (-1 === q.name.lastIndexOf(cli.casedQuery)) - if (q.name !== casedDomain) { - fail0x20 = q.name; - } - }); - - [ 'question', 'answer', 'authority', 'additional' ].forEach(function (group) { - (packet[group]||[]).forEach(function (a) { - var an = a.name; - var i = domain.toLowerCase().lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM - var j = a.name.toLowerCase().lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM - - // it's important to note that these should only relpace changes in casing that we expected - // any abnormalities should be left intact to go "huh?" about - // TODO detect abnormalities? - if (-1 !== i) { - // "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4)) - a.name = a.name.replace(casedDomain.substr(i), domain.substr(i)); - } else if (-1 !== j) { - // "www.example.com".replace("EXamPLE.cOm", "example.com") - a.name = a.name.substr(0, j) + a.name.substr(j).replace(casedDomain, domain); - } - - // NOTE: right now this assumes that anything matching the query matches all the way to the end - // it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly - // (but I don't think it should need to) - if (a.name.length !== an.length) { - console.error("[ERROR] question / answer mismatch: '" + an + "' != '" + a.length + "'"); - console.error(a); - } - }); - }); - - if (fail0x20) { - console.warn(";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" - + casedDomain + "' but got response for '" + fail0x20 + "'."); - return; - } - - res.send({ - header: packet.header - , question: packet.question - , answer: packet.answer - , authority: packet.authority - , additional: packet.additional - , edns_options: packet.edns_options - }); - } - , onListening: function () {} - , onSent: function (/*res*/) { } - , onTimeout: function (res) { - console.error('dns timeout:', res); - next(new Error("DNS timeout - no response")); - } - , onClose: function () { } - //, mdns: cli.mdns - , nameserver: nameserver - , port: 53 - , timeout: 2000 - }; - - dig.resolveJson(query, opts); -}); -app.get('/api/http', function (req, res) { - var url = req.query.url; - return request({ method: 'GET', url: url }).then(function (resp) { - res.send(resp.body); - }); -}); -app.get('/api/_acme_api_', function (req, res) { - res.send({ success: true }); -}); - -module.exports = app; -if (require.main === module) { - // curl -L http://localhost:3000/api/dns/example.com?type=A - console.info("Listening on localhost:3000"); - app.listen(3000); - console.info("Try this:"); - console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); - console.info("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); - console.info("\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"); -} diff --git a/tests/index.js b/tests/index.js index 36610e8..27d9bbd 100644 --- a/tests/index.js +++ b/tests/index.js @@ -2,6 +2,7 @@ require('dotenv').config(); +var punycode = require('punycode'); var ACME = require('../'); var Keypairs = require('../lib/keypairs.js'); var acme = ACME.create({ @@ -73,7 +74,7 @@ async function happyPath(accKty, srvKty, rnd) { if (config.debug) { console.info('ACME.js initialized'); console.info(metadata); - console.info(''); + console.info(); console.info(); } @@ -81,7 +82,7 @@ async function happyPath(accKty, srvKty, rnd) { if (config.debug) { console.info('Account Key Created'); console.info(JSON.stringify(accountKeypair, null, 2)); - console.info(''); + console.info(); console.info(); } @@ -96,7 +97,7 @@ async function happyPath(accKty, srvKty, rnd) { if (config.debug) { console.info('Agreeing to Terms of Service:'); console.info(tos); - console.info(''); + console.info(); console.info(); } agreed = true; @@ -123,7 +124,18 @@ async function happyPath(accKty, srvKty, rnd) { var domains = randomDomains(rnd); if (config.debug) { console.info('Get certificates for random domains:'); - console.info(domains); + console.info( + domains + .map(function(puny) { + var uni = punycode.toUnicode(puny); + if (puny !== uni) { + return puny + ' (' + uni + ')'; + } + return puny; + }) + .join('\n') + ); + console.info(); } var results = await acme.certificates.create({ account: account, @@ -140,8 +152,8 @@ async function happyPath(accKty, srvKty, rnd) { console.info(results.expires); console.info(results.cert); console.info(results.chain); - console.info(''); - console.info(''); + console.info(); + console.info(); } } @@ -163,18 +175,20 @@ happyPath('EC', 'RSA', rnd) function randomDomains(rnd) { return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map( function(pre) { - return pre + '-' + rnd + '.' + config.domain; + return punycode.toASCII(pre + '-' + rnd + '.' + config.domain); } ); } function random() { - return parseInt( - Math.random() - .toString() - .slice(2, 99), - 10 - ) - .toString(16) - .slice(0, 4); + return ( + parseInt( + Math.random() + .toString() + .slice(2, 99), + 10 + ) + .toString(16) + .slice(0, 4) + '例' + ); } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..e41d04a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,18 @@ +'use strict'; + +var path = require('path'); + +module.exports = { + entry: './lib/acme.js', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'acme.js', + library: 'acme', + libraryTarget: 'umd', + globalObject: "typeof self !== 'undefined' ? self : this" + }, + resolve: { + aliasFields: ['webpack', 'browser'], + mainFields: ['browser'] + } +};