Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
|
bef931f28f | |
|
eb432571ca | |
|
29a47e8fa4 | |
|
87e3555a5a | |
|
569c922eb0 | |
|
d10482697b | |
|
aa324e2a29 | |
|
e8c46db062 | |
|
6352961fea | |
|
333605d9b8 | |
|
86068fe015 | |
|
cf0ee1c064 | |
|
606dcf3c4f | |
|
0803517711 | |
|
0b91d9a26d | |
|
0743aa5280 | |
|
e388bc31bc | |
|
754c623cd1 | |
|
0107bc1d1f | |
|
293d950d8c | |
|
d6a3a7939b | |
|
fcbffdc0f9 | |
|
e447d71112 |
81
README.md
81
README.md
|
@ -2,10 +2,33 @@
|
|||
|
||||
| Built by [Root](https://therootcompany.com) for [Hub](https://rootprojects.org/hub)
|
||||
|
||||
ACME.js is a _low-level_ client for Let's Encrypt.
|
||||
## Automated Certificate Management Environment
|
||||
|
||||
ACME ([RFC 8555](https://tools.ietf.org/html/rfc8555)) is the protocol that powers **Let's Encrypt**.
|
||||
|
||||
ACME.js is a _low-level_ client that speaks RFC 8555 to get Free SSL certificates through Let's Encrypt.
|
||||
|
||||
Looking for an **easy**, _high-level_ client? Check out [Greenlock.js](https://git.rootprojects.org/root/greenlock.js).
|
||||
|
||||
# Quick Start
|
||||
|
||||
```js
|
||||
var acme = ACME.create({ maintainerEmail, packageAgent, notify });
|
||||
await acme.init(directoryUrl);
|
||||
|
||||
// Create Let's Encrypt Account
|
||||
var accountOptions = { subscriberEmail, agreeToTerms, accountKey };
|
||||
var account = await acme.accounts.create(accountOptions);
|
||||
|
||||
// Validate Domains
|
||||
var certificateOptions = { account, accountKey, csr, domains, challenges };
|
||||
var pems = await acme.certificates.create(certificateOptions);
|
||||
|
||||
// Get SSL Certificate
|
||||
var fullchain = pems.cert + '\n' + pems.chain + '\n';
|
||||
await fs.promises.writeFile('fullchain.pem', fullchain, 'ascii');
|
||||
```
|
||||
|
||||
# Online Demo
|
||||
|
||||
See https://greenlock.domains
|
||||
|
@ -80,7 +103,7 @@ The public API encapsulates the three high-level steps of the ACME protocol:
|
|||
- Challenge Presentation
|
||||
- Certificate Redemption
|
||||
|
||||
## Overview
|
||||
## API Overview
|
||||
|
||||
The core API can be show in just four functions:
|
||||
|
||||
|
@ -160,7 +183,9 @@ These `notify` events are intended for _logging_ and debugging, NOT as a data AP
|
|||
Note: DO NOT rely on **undocumented properties**. They are experimental and **will break**.
|
||||
If you have a use case for a particular property **open an issue** - we can lock it down and document it.
|
||||
|
||||
# Example
|
||||
# Example (Full Walkthrough)
|
||||
|
||||
### See [examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md)
|
||||
|
||||
A basic example includes the following:
|
||||
|
||||
|
@ -181,7 +206,8 @@ A basic example includes the following:
|
|||
- sign CSR
|
||||
- order certificate
|
||||
|
||||
See [examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md)
|
||||
[examples/README.md](https://git.rootprojects.org/root/acme.js/src/branch/master/examples/README.md)
|
||||
covers all of these steps, with comments.
|
||||
|
||||
# Install
|
||||
|
||||
|
@ -189,7 +215,7 @@ To make it easy to generate, encode, and decode keys and certificates,
|
|||
ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js)
|
||||
and [CSR.js](https://git.rootprojects.org/root/csr.js)
|
||||
|
||||
<detail>
|
||||
<details>
|
||||
<summary>Node.js</summary>
|
||||
|
||||
```js
|
||||
|
@ -200,9 +226,9 @@ npm install --save @root/acme
|
|||
var ACME = require('@root/acme');
|
||||
```
|
||||
|
||||
</detail>
|
||||
</details>
|
||||
|
||||
<detail>
|
||||
<details>
|
||||
<summary>WebPack</summary>
|
||||
|
||||
```html
|
||||
|
@ -215,9 +241,9 @@ var ACME = require('@root/acme');
|
|||
var ACME = require('@root/acme');
|
||||
```
|
||||
|
||||
</detail>
|
||||
</details>
|
||||
|
||||
<detail>
|
||||
<details>
|
||||
<summary>Vanilla JS</summary>
|
||||
|
||||
```html
|
||||
|
@ -242,7 +268,7 @@ Use
|
|||
var ACME = window['@root/acme'];
|
||||
```
|
||||
|
||||
</detail>
|
||||
</details>
|
||||
|
||||
# Challenge Callbacks
|
||||
|
||||
|
@ -354,18 +380,49 @@ node tests/index.js
|
|||
|
||||
Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us.
|
||||
|
||||
# Contributions
|
||||
|
||||
Did this project save you some time? Maybe make your day? Even save the day?
|
||||
|
||||
Please say "thanks" via Paypal or Patreon:
|
||||
|
||||
- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount: <paypal@therootcompany.com>
|
||||
- Patreon: <https://patreon.com/rootprojects>
|
||||
|
||||
Where does your contribution go?
|
||||
|
||||
[Root](https://therootcompany.com) is a collection of experts
|
||||
who trust each other and enjoy working together on deep-tech,
|
||||
Indie Web projects.
|
||||
|
||||
Our goal is to operate as a sustainable community.
|
||||
|
||||
Your contributions - both in code and _especially_ financially -
|
||||
help to not just this project, but also our broader work
|
||||
of [projects](https://rootprojects.org) that fuel the **Indie Web**.
|
||||
|
||||
Also, we chat on [Keybase](https://keybase.io)
|
||||
in [#rootprojects](https://keybase.io/team/rootprojects)
|
||||
|
||||
# Commercial Support
|
||||
|
||||
We have both commercial support and commercial licensing available.
|
||||
Do you need...
|
||||
|
||||
- more features?
|
||||
- bugfixes, on _your_ timeline?
|
||||
- custom code, built by experts?
|
||||
- commercial support and licensing?
|
||||
|
||||
You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem,
|
||||
Enterprise, and Internal installations, integrations, and deployments.
|
||||
|
||||
We have both commercial support and commercial licensing available.
|
||||
|
||||
We also offer consulting for all-things-ACME and Let's Encrypt.
|
||||
|
||||
# Legal & Rules of the Road
|
||||
|
||||
ACME.jsk™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
|
||||
ACME.js™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal
|
||||
|
||||
The rule of thumb is "attribute, but don't confuse". For example:
|
||||
|
||||
|
|
21
account.js
21
account.js
|
@ -5,12 +5,13 @@ var U = require('./utils.js');
|
|||
|
||||
var Keypairs = require('@root/keypairs');
|
||||
var Enc = require('@root/encoding/bytes');
|
||||
var agreers = {};
|
||||
|
||||
A._getAccountKid = function (me, options) {
|
||||
// It's just fine if there's no account, we'll go get the key id we need via the existing key
|
||||
var kid =
|
||||
options.kid ||
|
||||
(options.account && (options.account.key && options.account.key.kid));
|
||||
(options.account && options.account.key && options.account.key.kid);
|
||||
|
||||
if (kid) {
|
||||
return Promise.resolve(kid);
|
||||
|
@ -138,8 +139,15 @@ A._registerAccount = function(me, options) {
|
|||
var agreeToTerms = options.agreeToTerms;
|
||||
if (!agreeToTerms) {
|
||||
agreeToTerms = function (terms) {
|
||||
console.log(
|
||||
'By using this software you accept this Subscriber Agreement and Terms of Service:'
|
||||
if (agreers[options.subscriberEmail]) {
|
||||
return true;
|
||||
}
|
||||
agreers[options.subscriberEmail] = true;
|
||||
console.info();
|
||||
console.info(
|
||||
'By using this software you (' +
|
||||
options.subscriberEmail +
|
||||
') are agreeing to the following:'
|
||||
);
|
||||
console.info(
|
||||
'ACME Subscriber Agreement:',
|
||||
|
@ -147,8 +155,9 @@ A._registerAccount = function(me, options) {
|
|||
);
|
||||
console.info(
|
||||
'Greenlock/ACME.js Terms of Use:',
|
||||
terms.terms.acmeJsTermsUrl
|
||||
terms.acmeJsTermsUrl
|
||||
);
|
||||
console.info();
|
||||
return true;
|
||||
};
|
||||
} else if (true === agreeToTerms) {
|
||||
|
@ -157,8 +166,8 @@ A._registerAccount = function(me, options) {
|
|||
};
|
||||
}
|
||||
return agreeToTerms({
|
||||
acmeSubscriberTosUrl: me._tos,
|
||||
acmeJsTosUrl: 'https://rootprojects.org/legal/#terms'
|
||||
acmeSubscriberTermsUrl: me._tos,
|
||||
acmeJsTermsUrl: 'https://rootprojects.org/legal/#terms'
|
||||
});
|
||||
})
|
||||
.then(agree)
|
||||
|
|
40
acme.js
40
acme.js
|
@ -474,7 +474,7 @@ ACME._dryRun = function(me, realOptions, zonenames) {
|
|||
var selected = [];
|
||||
noopts.order._claims = claims.slice(0);
|
||||
noopts.notify = function (ev, params) {
|
||||
if ('challenge_select' === ev) {
|
||||
if ('_challenge_select' === ev) {
|
||||
selected.push(params.challenge);
|
||||
}
|
||||
};
|
||||
|
@ -486,6 +486,7 @@ ACME._dryRun = function(me, realOptions, zonenames) {
|
|||
type: ch.type
|
||||
//challenge: ch
|
||||
});
|
||||
// ignore promise return
|
||||
noopts.challenges[ch.type]
|
||||
.remove({ challenge: ch })
|
||||
.catch(function (err) {
|
||||
|
@ -774,7 +775,14 @@ ACME._postChallenge = function(me, options, kid, auth) {
|
|||
// REMOVE DNS records as soon as the state is non-processing
|
||||
// (valid or invalid or other)
|
||||
try {
|
||||
options.challenges[auth.type].remove({ challenge: auth });
|
||||
options.challenges[auth.type]
|
||||
.remove({ challenge: auth })
|
||||
.catch(function (err) {
|
||||
err.action = 'challenge_remove';
|
||||
err.altname = auth.altname;
|
||||
err.type = auth.type;
|
||||
ACME._notify(me, options, 'error', err);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
if ('valid' === resp.body.status) {
|
||||
|
@ -893,6 +901,15 @@ ACME._setChallenges = function(me, options, order) {
|
|||
placed.push(selected);
|
||||
ACME._notify(me, options, 'challenge_select', {
|
||||
// API-locked
|
||||
altname: ACME._untame(
|
||||
claim.identifier.value,
|
||||
claim.wildcard
|
||||
),
|
||||
type: selected.type,
|
||||
dnsHost: selected.dnsHost,
|
||||
keyAuthorization: selected.keyAuthorization
|
||||
});
|
||||
ACME._notify(me, options, '_challenge_select', {
|
||||
altname: ACME._untame(
|
||||
claim.identifier.value,
|
||||
claim.wildcard
|
||||
|
@ -1219,14 +1236,8 @@ ACME._prepRequest = function(me, options) {
|
|||
options.domains = options.domains || _csr.altnames;
|
||||
_csr.altnames = _csr.altnames || [];
|
||||
if (
|
||||
options.domains
|
||||
.slice(0)
|
||||
.sort()
|
||||
.join(' ') !==
|
||||
_csr.altnames
|
||||
.slice(0)
|
||||
.sort()
|
||||
.join(' ')
|
||||
options.domains.slice(0).sort().join(' ') !==
|
||||
_csr.altnames.slice(0).sort().join(' ')
|
||||
) {
|
||||
return Promise.reject(
|
||||
new Error('certificate altnames do not match requested domains')
|
||||
|
@ -1330,10 +1341,7 @@ ACME._csrToUrlBase64 = function(csr) {
|
|||
// TODO use PEM.parseBlock()
|
||||
// nix PEM headers, if any
|
||||
if ('-' === csr[0]) {
|
||||
csr = csr
|
||||
.split(/\n+/)
|
||||
.slice(1, -1)
|
||||
.join('');
|
||||
csr = csr.split(/\n+/).slice(1, -1).join('');
|
||||
}
|
||||
return Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, ''));
|
||||
};
|
||||
|
@ -1342,9 +1350,7 @@ ACME._csrToUrlBase64 = function(csr) {
|
|||
ACME._prnd = function (n) {
|
||||
var rnd = '';
|
||||
while (rnd.length / 2 < n) {
|
||||
var i = Math.random()
|
||||
.toString()
|
||||
.substr(2);
|
||||
var i = Math.random().toString().substr(2);
|
||||
var h = parseInt(i, 10).toString(16);
|
||||
if (h.length % 2) {
|
||||
h = '0' + h;
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
var https = require('http2');
|
||||
var tls = require('tls');
|
||||
var fs = require('fs');
|
||||
|
||||
var key = fs.readFileSync('./privkey.pem');
|
||||
var cert = fs.readFileSync('./fullchain.pem');
|
||||
|
||||
function SNICallback(servername, cb) {
|
||||
console.log('sni:', servername);
|
||||
cb(null, tls.createSecureContext({ key, cert }));
|
||||
}
|
||||
|
||||
var server = https
|
||||
.createSecureServer({ SNICallback: SNICallback }, function (req, res) {
|
||||
res.end('Hello, Encrypted World!');
|
||||
})
|
||||
.listen(443, function () {
|
||||
console.info('Listening on', server.address());
|
||||
});
|
|
@ -9,9 +9,6 @@ sha2.sum = function(alg, str) {
|
|||
var sha = 'sha' + String(alg).replace(/^sha-?/i, '');
|
||||
// utf8 is the default for strings
|
||||
var buf = Buffer.from(str);
|
||||
return crypto
|
||||
.createHash(sha)
|
||||
.update(buf)
|
||||
.digest();
|
||||
return crypto.createHash(sha).update(buf).digest();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -33,9 +33,10 @@ M.init = function(me) {
|
|||
};
|
||||
|
||||
M._init = function (me, tz, locale) {
|
||||
// prevent a stampede from misconfigured clients in an eternal loop
|
||||
setTimeout(function () {
|
||||
// prevent a stampede from misconfigured clients in an eternal loop
|
||||
me.request({
|
||||
timeout: 3000,
|
||||
method: 'GET',
|
||||
url: 'https://api.rootprojects.org/api/nonce',
|
||||
json: true
|
||||
|
@ -47,6 +48,7 @@ M._init = function(me, tz, locale) {
|
|||
})
|
||||
.then(function (hashcash) {
|
||||
var req = {
|
||||
timeout: 3000,
|
||||
headers: {
|
||||
'x-root-nonce-v1': hashcash
|
||||
},
|
||||
|
@ -60,10 +62,13 @@ M._init = function(me, tz, locale) {
|
|||
locale: locale
|
||||
}
|
||||
};
|
||||
return me
|
||||
.request(req)
|
||||
return me.request(req);
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (true || me.debug) {
|
||||
if (me.debug) {
|
||||
console.error(
|
||||
'error adding maintainer to support notices:'
|
||||
);
|
||||
console.error(err);
|
||||
}
|
||||
})
|
||||
|
@ -71,7 +76,6 @@ M._init = function(me, tz, locale) {
|
|||
oldCollegeTries[me.maintainerEmail] = true;
|
||||
//console.log(resp);
|
||||
});
|
||||
});
|
||||
}, me.__timeout || 3000);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@root/acme",
|
||||
"version": "3.0.1",
|
||||
"version": "3.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -16,7 +16,6 @@
|
|||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz",
|
||||
"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@root/asn1": "^1.0.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
|
@ -29,9 +28,9 @@
|
|||
"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
|
||||
},
|
||||
"@root/keypairs": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz",
|
||||
"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==",
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.10.0.tgz",
|
||||
"integrity": "sha512-t8VocY46Mtb0NTsxzyLLf5tsgfw0BXLYVADAyiRdEdqHcvPFGJdjkXNtHVQuSV/FMaC65iTOHVP4E6X8iT3Ikg==",
|
||||
"requires": {
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/pem": "^1.0.4",
|
||||
|
@ -44,9 +43,9 @@
|
|||
"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA=="
|
||||
},
|
||||
"@root/request": {
|
||||
"version": "1.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz",
|
||||
"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw=="
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@root/request/-/request-1.6.1.tgz",
|
||||
"integrity": "sha512-8wrWyeBLRp7T8J36GkT3RODJ6zYmL0/maWlAUD5LOXT28D3TDquUepyYDKYANNA3Gc8R5ZCgf+AXvSTYpJEWwQ=="
|
||||
},
|
||||
"@root/x509": {
|
||||
"version": "0.7.2",
|
||||
|
@ -153,9 +152,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
|
||||
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@root/acme",
|
||||
"version": "3.0.2",
|
||||
"version": "3.1.0",
|
||||
"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt",
|
||||
"homepage": "https://rootprojects.org/acme/",
|
||||
"main": "acme.js",
|
||||
|
@ -42,14 +42,14 @@
|
|||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@root/csr": "^0.8.1",
|
||||
"@root/encoding": "^1.0.1",
|
||||
"@root/keypairs": "^0.9.0",
|
||||
"@root/keypairs": "^0.10.0",
|
||||
"@root/pem": "^1.0.4",
|
||||
"@root/request": "^1.3.11",
|
||||
"@root/request": "^1.6.1",
|
||||
"@root/x509": "^0.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@root/csr": "^0.8.1",
|
||||
"dig.js": "^1.3.9",
|
||||
"dns-suite": "^1.2.13",
|
||||
"dotenv": "^8.1.0",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// TODO put postinstall back
|
||||
// TODO put postinstall message
|
||||
|
|
|
@ -247,12 +247,7 @@ module.exports = function() {
|
|||
|
||||
function random() {
|
||||
return (
|
||||
parseInt(
|
||||
Math.random()
|
||||
.toString()
|
||||
.slice(2, 99),
|
||||
10
|
||||
)
|
||||
parseInt(Math.random().toString().slice(2, 99), 10)
|
||||
.toString(16)
|
||||
.slice(0, 4) + '例'
|
||||
);
|
||||
|
|
|
@ -33,10 +33,7 @@ native
|
|||
|
||||
var now = Date.now();
|
||||
var nonce = '20';
|
||||
var needle = crypto
|
||||
.randomBytes(3)
|
||||
.toString('hex')
|
||||
.slice(0, 5);
|
||||
var needle = crypto.randomBytes(3).toString('hex').slice(0, 5);
|
||||
native
|
||||
._hashcash({
|
||||
alg: 'SHA-256',
|
||||
|
|
9
utils.js
9
utils.js
|
@ -11,12 +11,13 @@ U._jwsRequest = function(me, bigopts) {
|
|||
bigopts.protected.nonce = nonce;
|
||||
bigopts.protected.url = bigopts.url;
|
||||
// protected.alg: added by Keypairs.signJws
|
||||
if (!bigopts.protected.jwk) {
|
||||
// protected.kid must be overwritten due to ACME's interpretation of the spec
|
||||
if (!('kid' in bigopts.protected)) {
|
||||
if (bigopts.protected.jwk) {
|
||||
bigopts.protected.kid = false;
|
||||
} else if (!('kid' in bigopts.protected)) {
|
||||
// protected.kid must be provided according to ACME's interpretation of the spec
|
||||
// (using the provided URL rather than the Key's Thumbprint as Key ID)
|
||||
bigopts.protected.kid = bigopts.kid;
|
||||
}
|
||||
}
|
||||
|
||||
// this will shasum the thumbprint the 2nd time
|
||||
return Keypairs.signJws({
|
||||
|
|
Loading…
Reference in New Issue