forked from root/acme.js
ready-ish to release Bluecrypt ACME
This commit is contained in:
parent
14c24e3aea
commit
b324afe6d6
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
|||
Copyright 2017-present AJ ONeal
|
||||
Copyright 2015-2019 AJ ONeal
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
|
195
README.md
195
README.md
|
@ -1,9 +1,190 @@
|
|||
# Bluecrypt™ Keypairs
|
||||
# Bluecrypt™ [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) | A [Root](https://rootprojects.org/acme/) project
|
||||
|
||||
A port of [keypairs.js](https://git.coolaj86.com/coolaj86/keypairs.js) to the browser.
|
||||
Free SSL Certificates from Let's Encrypt, right in your Web Browser
|
||||
|
||||
* Keypairs
|
||||
* Eckles (ECDSA)
|
||||
* Rasha (RSA)
|
||||
* X509
|
||||
* ASN1
|
||||
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
|
||||
|
||||
# Online Demos
|
||||
|
||||
* Greenlock for the Web <https://greenlock.domains>
|
||||
* Bluecrypt ACME Demo <https://rootprojects.org/acme/>
|
||||
|
||||
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.
|
||||
|
||||
We'd much rather improve the app than have a hundred different versions running in the wild.
|
||||
However, in keeping to our values we've made the source visible for others to inspect, improve, and modify.
|
||||
|
||||
# QuickStart
|
||||
|
||||
Bluecrypt ACME 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
|
||||
<script src="https://rootprojects.org/acme/bluecrypt-acme.js"></script>
|
||||
```
|
||||
|
||||
`bluecrypt-acme.min.js`
|
||||
```html
|
||||
<script src="https://rootprojects.org/acme/bluecrypt-acme.min.js"></script>
|
||||
```
|
||||
|
||||
You can see `index.html` and `app.js` in the repo for full example usage.
|
||||
|
||||
### Instantiate Bluecrypt ACME
|
||||
|
||||
Although built for Let's Encrypt, Bluecrypt ACME 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;
|
||||
});
|
||||
```
|
||||
|
||||
### Create ACME Account with Let's Encrypt
|
||||
|
||||
ACME Accounts are key and device based, with an email address as a backup identifier.
|
||||
|
||||
A public account key must be registered before an SSL certificate can be requested.
|
||||
|
||||
```js
|
||||
var accountPrivateKey;
|
||||
var account;
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Get Free 90-day SSL Certificate
|
||||
|
||||
Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key,
|
||||
the names of domains to be secured, and a distinctly separate server private key.
|
||||
|
||||
A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record)
|
||||
is a required part of the process, which requires `set` and `remove` callbacks/promises.
|
||||
|
||||
```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);
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
### Example "Challenge" Implementation
|
||||
|
||||
Typically here you're just presenting some sort of dialog to the user to ask them to
|
||||
upload a file or set a DNS record.
|
||||
|
||||
It may be possible to do something fancy like using OAuth2 to login to Google Domanis
|
||||
to set a DNS address, etc, but it seems like that sort of fanciness is probably best
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# Full Documentation
|
||||
|
||||
See [acme.js](https://git.coolaj86.com/coolaj86/acme-v2.js).
|
||||
|
||||
Aside from the loading instructions (`npm` and `require` instead of `script` tags),
|
||||
the usage is identical to the node version.
|
||||
|
||||
That said, the two may leap-frog a little from time to time
|
||||
(for example, the browser version is just a touch ahead at the moment).
|
||||
|
||||
# Developing
|
||||
|
||||
You can see `<script>` tags in the `index.html` in the repo, which references the original
|
||||
source files.
|
||||
|
||||
Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us.
|
||||
|
||||
# Commercial Support
|
||||
|
||||
We have both commercial support and commercial licensing available.
|
||||
|
||||
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
|
||||
Enterprise, and Internal installations, integrations, and deployments.
|
||||
|
||||
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
|
||||
|
||||
The rule of thumb is "attribute, but don't confuse". For example:
|
||||
|
||||
> Built with [Root](https://rootprojects.org)'s [Bluecrypt ACME](https://git.rootprojects.org/root/bluecrypt-acme.js).
|
||||
|
||||
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 help to community as we build great software.
|
||||
|
||||
[bluecrypt.js](https://git.coolaj86.com/coolaj86/bluecrypt.js) |
|
||||
MPL-2.0 |
|
||||
[Terms of Use](https://therootcompany.com/legal/#terms) |
|
||||
[Privacy Policy](https://therootcompany.com/legal/#privacy)
|
||||
|
|
156
app.js
156
app.js
|
@ -17,6 +17,14 @@
|
|||
return Array.prototype.slice.call(document.querySelectorAll(sel));
|
||||
}
|
||||
|
||||
function checkTos(tos) {
|
||||
if ($('input[name="tos"]:checked')) {
|
||||
return tos;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
console.log('hello');
|
||||
|
||||
|
@ -111,8 +119,156 @@
|
|||
});
|
||||
});
|
||||
|
||||
$('form.js-acme-account').addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
$('.js-loading').hidden = false;
|
||||
var acme = ACME.create({
|
||||
Keypairs: Keypairs
|
||||
, CSR: CSR
|
||||
});
|
||||
acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) {
|
||||
console.log('acme result', result);
|
||||
var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||
var email = $('.js-email').value;
|
||||
return acme.accounts.create({
|
||||
email: email
|
||||
, agreeToTerms: checkTos
|
||||
, accountKeypair: { privateKeyJwk: privJwk }
|
||||
}).then(function (account) {
|
||||
console.log("account created result:", account);
|
||||
accountStuff.account = account;
|
||||
accountStuff.privateJwk = privJwk;
|
||||
accountStuff.email = email;
|
||||
accountStuff.acme = acme;
|
||||
$('.js-create-order').hidden = false;
|
||||
$('.js-toc-acme-account-response').hidden = false;
|
||||
$('.js-acme-account-response').innerText = JSON.stringify(account, null, 2);
|
||||
}).catch(function (err) {
|
||||
console.error("A bad thing happened:");
|
||||
console.error(err);
|
||||
window.alert(err.message || JSON.stringify(err, null, 2));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('form.js-csr').addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
generateCsr();
|
||||
});
|
||||
|
||||
$('form.js-acme-order').addEventListener('submit', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var account = accountStuff.account;
|
||||
var privJwk = accountStuff.privateJwk;
|
||||
var email = accountStuff.email;
|
||||
var acme = accountStuff.acme;
|
||||
|
||||
|
||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
||||
return getDomainPrivkey().then(function (domainPrivJwk) {
|
||||
console.log('Has CSR already?');
|
||||
console.log(accountStuff.csr);
|
||||
return acme.certificates.create({
|
||||
accountKeypair: { privateKeyJwk: privJwk }
|
||||
, account: account
|
||||
, serverKeypair: { privateKeyJwk: domainPrivJwk }
|
||||
, csr: accountStuff.csr
|
||||
, domains: domains
|
||||
, skipDryRun: $('input[name="skip-dryrun"]:checked') && true
|
||||
, agreeToTerms: checkTos
|
||||
, challenges: {
|
||||
'dns-01': {
|
||||
set: function (opts) {
|
||||
console.info('dns-01 set challenge:');
|
||||
console.info('TXT', opts.dnsHost);
|
||||
console.info(opts.dnsAuthorization);
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you set the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
, remove: function (opts) {
|
||||
console.log('dns-01 remove challenge:');
|
||||
console.info('TXT', opts.dnsHost);
|
||||
console.info(opts.dnsAuthorization);
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you delete the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
, 'http-01': {
|
||||
set: function (opts) {
|
||||
console.info('http-01 set challenge:');
|
||||
console.info(opts.challengeUrl);
|
||||
console.info(opts.keyAuthorization);
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you set the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
, remove: function (opts) {
|
||||
console.log('http-01 remove challenge:');
|
||||
console.info(opts.challengeUrl);
|
||||
console.info(opts.keyAuthorization);
|
||||
return new Promise(function (resolve) {
|
||||
while (!window.confirm("Did you delete the challenge?")) {}
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
, challengeTypes: [$('input[name="acme-challenge-type"]:checked').value]
|
||||
}).then(function (results) {
|
||||
console.log('Got Certificates:');
|
||||
console.log(results);
|
||||
$('.js-toc-acme-order-response').hidden = false;
|
||||
$('.js-acme-order-response').innerText = JSON.stringify(results, null, 2);
|
||||
}).catch(function (err) {
|
||||
console.error("challenge failed:");
|
||||
console.error(err);
|
||||
window.alert("failed! " + err.message || JSON.stringify(err));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('.js-generate').hidden = false;
|
||||
}
|
||||
|
||||
function getDomainPrivkey() {
|
||||
if (accountStuff.domainPrivateJwk) { return Promise.resolve(accountStuff.domainPrivateJwk); }
|
||||
return Keypairs.generate({
|
||||
kty: $('input[name="kty"]:checked').value
|
||||
, namedCurve: $('input[name="ec-crv"]:checked').value
|
||||
, modulusLength: $('input[name="rsa-len"]:checked').value
|
||||
}).then(function (pair) {
|
||||
console.log('domain keypair:', pair);
|
||||
accountStuff.domainPrivateJwk = pair.private;
|
||||
return pair.private;
|
||||
});
|
||||
}
|
||||
|
||||
function generateCsr() {
|
||||
var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g);
|
||||
//var privJwk = JSON.parse($('.js-jwk').innerText).private;
|
||||
return getDomainPrivkey().then(function (privJwk) {
|
||||
accountStuff.domainPrivateJwk = privJwk;
|
||||
return CSR({ jwk: privJwk, domains: domains }).then(function (pem) {
|
||||
// Verify with https://www.sslshopper.com/csr-decoder.html
|
||||
accountStuff.csr = pem;
|
||||
console.log('Created CSR:');
|
||||
console.log(pem);
|
||||
|
||||
console.log('CSR info:');
|
||||
console.log(CSR._info(pem));
|
||||
|
||||
return pem;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', run);
|
||||
}());
|
||||
|
|
45
index.html
45
index.html
|
@ -53,6 +53,39 @@
|
|||
<button class="js-generate" hidden>Generate</button>
|
||||
</form>
|
||||
|
||||
<h2>ACME Account</h2>
|
||||
<form class="js-acme-account">
|
||||
<label for="-acmeEmail">Email:</label>
|
||||
<input class="js-email" type="email" id="-acmeEmail" value="john.doe@gmail.com">
|
||||
<br>
|
||||
<label for="-acmeTos"><input class="js-tos" name="tos" type="checkbox" id="-acmeTos" checked>
|
||||
Agree to Let's Encrypt Terms of Service</label>
|
||||
<br>
|
||||
<button class="js-create-account" hidden>Create Account</button>
|
||||
</form>
|
||||
|
||||
<h2>Certificate Signing Request</h2>
|
||||
<form class="js-csr">
|
||||
<label for="-acmeDomains">Domains:</label>
|
||||
<input class="js-domains" type="text" id="-acmeDomains" value="example.com www.example.com">
|
||||
<br>
|
||||
<button class="js-create-csr" hidden>Create CSR</button>
|
||||
</form>
|
||||
|
||||
<h2>ACME Certificate Order</h2>
|
||||
<form class="js-acme-order">
|
||||
Challenge type:
|
||||
<label for="-http01"><input type="radio" id="-http01"
|
||||
name="acme-challenge-type" value="http-01" checked>http-01</label>
|
||||
<label for="-dns01"><input type="radio" id="-dns01"
|
||||
name="acme-challenge-type" value="dns-01">dns-01</label>
|
||||
<br>
|
||||
<label for="-skipDryrun"><input class="js-skip-dryrun" name="skip-dryrun"
|
||||
type="checkbox" id="-skipDryrun" checked> Skip dry-run challenge</label>
|
||||
<br>
|
||||
<button class="js-create-order" hidden>Create Order</button>
|
||||
</form>
|
||||
|
||||
<div class="js-loading" hidden>Loading</div>
|
||||
|
||||
<details class="js-toc-jwk" hidden>
|
||||
|
@ -87,13 +120,23 @@
|
|||
<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary>
|
||||
<pre><code class="js-input-pem-spki-public" ></code></pre>
|
||||
</details>
|
||||
|
||||
<details class="js-toc-acme-account-response" hidden>
|
||||
<summary>ACME Account Request</summary>
|
||||
<pre><code class="js-acme-account-response"> </code></pre>
|
||||
</details>
|
||||
<details class="js-toc-acme-order-response" hidden>
|
||||
<summary>ACME Order Response</summary>
|
||||
<pre><code class="js-acme-order-response"> </code></pre>
|
||||
</details>
|
||||
<script src="./lib/bluecrypt-encoding.js"></script>
|
||||
<script src="./lib/asn1-packer.js"></script>
|
||||
<script src="./lib/x509.js"></script>
|
||||
<script src="./lib/ecdsa.js"></script>
|
||||
<script src="./lib/rsa.js"></script>
|
||||
<script src="./lib/keypairs.js"></script>
|
||||
<script src="./lib/asn1-parser.js"></script>
|
||||
<script src="./lib/csr.js"></script>
|
||||
<script src="./lib/acme.js"></script>
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,298 @@
|
|||
// Copyright 2018-present AJ ONeal. All rights reserved
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
(function (exports) {
|
||||
'use strict';
|
||||
/*global Promise*/
|
||||
|
||||
var ASN1 = exports.ASN1;
|
||||
var Enc = exports.Enc;
|
||||
var PEM = exports.PEM;
|
||||
var X509 = exports.x509;
|
||||
var Keypairs = exports.Keypairs;
|
||||
|
||||
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
|
||||
var CSR = exports.CSR = function (opts) {
|
||||
// We're using a Promise here to be compatible with the browser version
|
||||
// which will probably use the webcrypto API for some of the conversions
|
||||
return CSR._prepare(opts).then(function (opts) {
|
||||
return CSR.create(opts).then(function (bytes) {
|
||||
return CSR._encode(opts, bytes);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
CSR._prepare = function (opts) {
|
||||
return Promise.resolve().then(function () {
|
||||
var Keypairs;
|
||||
opts = JSON.parse(JSON.stringify(opts));
|
||||
|
||||
// We do a bit of extra error checking for user convenience
|
||||
if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); }
|
||||
if (!Array.isArray(opts.domains) || 0 === opts.domains.length) {
|
||||
new Error("You must pass options.domains as a non-empty array");
|
||||
}
|
||||
|
||||
// I need to check that 例.中国 is a valid domain name
|
||||
if (!opts.domains.every(function (d) {
|
||||
// allow punycode? xn--
|
||||
if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) {
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
throw new Error("You must pass options.domains as strings");
|
||||
}
|
||||
|
||||
if (opts.jwk) { return opts; }
|
||||
if (opts.key && opts.key.kty) {
|
||||
opts.jwk = opts.key;
|
||||
return opts;
|
||||
}
|
||||
if (!opts.pem && !opts.key) {
|
||||
throw new Error("You must pass options.key as a JSON web key");
|
||||
}
|
||||
|
||||
Keypairs = exports.Keypairs;
|
||||
if (!exports.Keypairs) {
|
||||
throw new Error("Keypairs.js is an optional dependency for PEM-to-JWK.\n"
|
||||
+ "Install it if you'd like to use it:\n"
|
||||
+ "\tnpm install --save rasha\n"
|
||||
+ "Otherwise supply a jwk as the private key."
|
||||
);
|
||||
}
|
||||
|
||||
return Keypairs.import({ pem: opts.pem || opts.key }).then(function (pair) {
|
||||
opts.jwk = pair.private;
|
||||
return opts;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
CSR._encode = function (opts, bytes) {
|
||||
if ('der' === (opts.encoding||'').toLowerCase()) {
|
||||
return bytes;
|
||||
}
|
||||
return PEM.packBlock({
|
||||
type: "CERTIFICATE REQUEST"
|
||||
, bytes: bytes /* { jwk: jwk, domains: opts.domains } */
|
||||
});
|
||||
};
|
||||
|
||||
CSR.create = function createCsr(opts) {
|
||||
var hex = CSR.request(opts.jwk, opts.domains);
|
||||
return CSR._sign(opts.jwk, hex).then(function (csr) {
|
||||
return Enc.hexToBuf(csr);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// EC / RSA
|
||||
//
|
||||
CSR.request = function createCsrBodyEc(jwk, domains) {
|
||||
var asn1pub;
|
||||
if (/^EC/i.test(jwk.kty)) {
|
||||
asn1pub = X509.packCsrEcPublicKey(jwk);
|
||||
} else {
|
||||
asn1pub = X509.packCsrRsaPublicKey(jwk);
|
||||
}
|
||||
return X509.packCsr(asn1pub, domains);
|
||||
};
|
||||
|
||||
CSR._sign = function csrEcSig(jwk, request) {
|
||||
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
||||
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
|
||||
// TODO have a consistent non-private way to sign
|
||||
return Keypairs._sign({ jwk: jwk, format: 'x509' }, Enc.hexToBuf(request)).then(function (sig) {
|
||||
return CSR._toDer({ request: request, signature: sig, kty: jwk.kty });
|
||||
});
|
||||
};
|
||||
|
||||
CSR._toDer = function encode(opts) {
|
||||
var sty;
|
||||
if (/^EC/i.test(opts.kty)) {
|
||||
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
|
||||
sty = ASN1('30', ASN1('06', '2a8648ce3d040302'));
|
||||
} else {
|
||||
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
|
||||
sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05'));
|
||||
}
|
||||
return ASN1('30'
|
||||
// The Full CSR Request Body
|
||||
, opts.request
|
||||
// The Signature Type
|
||||
, sty
|
||||
// The Signature
|
||||
, ASN1.BitStr(Enc.bufToHex(opts.signature))
|
||||
);
|
||||
};
|
||||
|
||||
X509.packCsr = function (asn1pubkey, domains) {
|
||||
return ASN1('30'
|
||||
// Version (0)
|
||||
, ASN1.UInt('00')
|
||||
|
||||
// 2.5.4.3 commonName (X.520 DN component)
|
||||
, ASN1('30', ASN1('31', ASN1('30', ASN1('06', '550403'), ASN1('0c', Enc.utf8ToHex(domains[0])))))
|
||||
|
||||
// Public Key (RSA or EC)
|
||||
, asn1pubkey
|
||||
|
||||
// Request Body
|
||||
, ASN1('a0'
|
||||
, ASN1('30'
|
||||
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
|
||||
, ASN1('06', '2a864886f70d01090e')
|
||||
, ASN1('31'
|
||||
, ASN1('30'
|
||||
, ASN1('30'
|
||||
// 2.5.29.17 subjectAltName (X.509 extension)
|
||||
, ASN1('06', '551d11')
|
||||
, ASN1('04'
|
||||
, ASN1('30', domains.map(function (d) {
|
||||
return ASN1('82', Enc.utf8ToHex(d));
|
||||
}).join(''))))))))
|
||||
);
|
||||
};
|
||||
|
||||
// TODO finish this later
|
||||
// we want to parse the domains, the public key, and verify the signature
|
||||
CSR._info = function (der) {
|
||||
// standard base64 PEM
|
||||
if ('string' === typeof der && '-' === der[0]) {
|
||||
der = PEM.parseBlock(der).bytes;
|
||||
}
|
||||
// jose urlBase64 not-PEM
|
||||
if ('string' === typeof der) {
|
||||
der = Enc.base64ToBuf(der);
|
||||
}
|
||||
// not supporting binary-encoded bas64
|
||||
var c = ASN1.parse(der);
|
||||
var kty;
|
||||
// A cert has 3 parts: cert, signature meta, signature
|
||||
if (c.children.length !== 3) {
|
||||
throw new Error("doesn't look like a certificate request: expected 3 parts of header");
|
||||
}
|
||||
var sig = c.children[2];
|
||||
if (sig.children.length) {
|
||||
// ASN1/X509 EC
|
||||
sig = sig.children[0];
|
||||
sig = ASN1('30', ASN1.UInt(Enc.bufToHex(sig.children[0].value)), ASN1.UInt(Enc.bufToHex(sig.children[1].value)));
|
||||
sig = Enc.hexToBuf(sig);
|
||||
kty = 'EC';
|
||||
} else {
|
||||
// Raw RSA Sig
|
||||
sig = sig.value;
|
||||
kty = 'RSA';
|
||||
}
|
||||
//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");
|
||||
}
|
||||
// 0 null
|
||||
// 1 commonName / subject
|
||||
var sub = Enc.bufToBin(req.children[1].children[0].children[0].children[1].value);
|
||||
// 3 public key (type, key)
|
||||
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
|
||||
var pub;
|
||||
// TODO reuse ASN1 parser for these?
|
||||
if ('EC' === kty) {
|
||||
// throw away compression byte
|
||||
pub = req.children[2].children[1].value.slice(1);
|
||||
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
|
||||
while (0 === pub.x[0]) { pub.x = pub.x.slice(1); }
|
||||
while (0 === pub.y[0]) { pub.y = pub.y.slice(1); }
|
||||
if ((pub.x.length || pub.x.byteLength) > 48) {
|
||||
pub.crv = 'P-521';
|
||||
} else if ((pub.x.length || pub.x.byteLength) > 32) {
|
||||
pub.crv = 'P-384';
|
||||
} else {
|
||||
pub.crv = 'P-256';
|
||||
}
|
||||
pub.x = Enc.bufToUrlBase64(pub.x);
|
||||
pub.y = Enc.bufToUrlBase64(pub.y);
|
||||
} else {
|
||||
pub = req.children[2].children[1].children[0];
|
||||
pub = { kty: kty, n: pub.children[0].value, e: pub.children[1].value };
|
||||
while (0 === pub.n[0]) { pub.n = pub.n.slice(1); }
|
||||
while (0 === pub.e[0]) { pub.e = pub.e.slice(1); }
|
||||
pub.n = Enc.bufToUrlBase64(pub.n);
|
||||
pub.e = Enc.bufToUrlBase64(pub.e);
|
||||
}
|
||||
// 4 extensions
|
||||
var domains = req.children[3].children.filter(function (seq) {
|
||||
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
|
||||
if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
|
||||
return true;
|
||||
}
|
||||
}).map(function (seq) {
|
||||
return seq.children[1].children[0].children.filter(function (seq2) {
|
||||
// subjectAltName (X.509 extension)
|
||||
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
|
||||
return true;
|
||||
}
|
||||
}).map(function (seq2) {
|
||||
return seq2.children[1].children[0].children.map(function (name) {
|
||||
// TODO utf8
|
||||
return Enc.bufToBin(name.value);
|
||||
});
|
||||
})[0];
|
||||
})[0];
|
||||
|
||||
return {
|
||||
subject: sub
|
||||
, altnames: domains
|
||||
, jwk: pub
|
||||
, signature: sig
|
||||
};
|
||||
};
|
||||
|
||||
X509.packCsrRsaPublicKey = function (jwk) {
|
||||
// Sequence the key
|
||||
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
|
||||
var e = ASN1.UInt(Enc.base64ToHex(jwk.e));
|
||||
var asn1pub = ASN1('30', n, e);
|
||||
|
||||
// Add the CSR pub key header
|
||||
return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub));
|
||||
};
|
||||
|
||||
X509.packCsrEcPublicKey = function (jwk) {
|
||||
var ecOid = X509._oids[jwk.crv];
|
||||
if (!ecOid) {
|
||||
throw new Error("Unsupported namedCurve '" + jwk.crv + "'. Supported types are " + Object.keys(X509._oids));
|
||||
}
|
||||
var cmp = '04'; // 04 == x+y, 02 == x-only
|
||||
var hxy = '';
|
||||
// Placeholder. I'm not even sure if compression should be supported.
|
||||
if (!jwk.y) { cmp = '02'; }
|
||||
hxy += Enc.base64ToHex(jwk.x);
|
||||
if (jwk.y) { hxy += Enc.base64ToHex(jwk.y); }
|
||||
|
||||
// 1.2.840.10045.2.1 ecPublicKey
|
||||
return ASN1('30', ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)), ASN1.BitStr(cmp + hxy));
|
||||
};
|
||||
X509._oids = {
|
||||
// 1.2.840.10045.3.1.7 prime256v1
|
||||
// (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07)
|
||||
'P-256': '2a8648ce3d030107'
|
||||
// 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22)
|
||||
// (SEC 2 recommended EC domain secp256r1)
|
||||
, 'P-384': '2b81040022'
|
||||
// requires more logic and isn't a recommended standard
|
||||
// 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23)
|
||||
// (SEC 2 alternate P-521)
|
||||
//, 'P-521': '2B 81 04 00 23'
|
||||
};
|
||||
|
||||
// don't replace the full parseBlock, if it exists
|
||||
PEM.parseBlock = PEM.parseBlock || function (str) {
|
||||
var der = str.split(/\n/).filter(function (line) {
|
||||
return !/-----/.test(line);
|
||||
}).join('');
|
||||
return { bytes: Enc.base64ToBuf(der) };
|
||||
};
|
||||
|
||||
}('undefined' === typeof window ? module.exports : window));
|
|
@ -0,0 +1,553 @@
|
|||
{
|
||||
"name": "bluecrypt-keypairs",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"bluebird": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
|
||||
"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",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"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",
|
||||
"integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"exit": "0.1.2",
|
||||
"glob": "^7.1.1"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha512-O/tSWZuW7AwpjsgePPmTanwvSDL9xF+FzLTJD9byN3C6lk79iMejC/Ahz9CERAXTW4e2TXL1vtqh3T0Ug79ocA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cli": "^1.0.1",
|
||||
"dns-suite": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
|
||||
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"dns-suite": {
|
||||
"version": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#092008f766540909d27c934211495c9e03705bf3",
|
||||
"from": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bluebird": "^3.5.0",
|
||||
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dns-suite": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.12.tgz",
|
||||
"integrity": "sha512-K4LWqmJT/T2QLaGCJ+qRvrT9DicKs5XxXYXw6uIZ1apdwyfToQk7K9AZbpFd0FLRdZG809v2vAcsquPbQh+Ipg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"bluebird": "^3.5.0",
|
||||
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
|
||||
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"hexdump.js": {
|
||||
"version": "git+https://git.coolaj86.com/coolaj86/hexdump.js#222fa7de5036a16397de2fe703c35ac54a3d8d0c",
|
||||
"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",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"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",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"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
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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=",
|
||||
"dev": true
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"name": "bluecrypt-keypairs",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.0",
|
||||
"description": "Zero-Dependency Native Browser support for ECDSA P-256 and P-384, and RSA 2048/3072/4096 written in VanillaJS",
|
||||
"main": "server.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
|
@ -28,5 +29,9 @@
|
|||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@root/request": "^1.3.10",
|
||||
"dig.js": "^1.3.9",
|
||||
"dns-suite": "^1.2.12",
|
||||
"express": "^4.16.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
'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'");
|
||||
}
|
Loading…
Reference in New Issue