mirror of https://github.com/certd/certd
🔱: [acme] sync upgrade with 3 commits [trident-sync]
Bump v5.3.0 Example for dns-01pull/29/head
parent
80cd1bfc8e
commit
960f61d158
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
## v5.3.0
|
||||
## v5.3.0 (2024-02-05)
|
||||
|
||||
* `added` Support and tests for satisfying `tls-alpn-01` challenges
|
||||
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
|
||||
|
|
|
@ -132,7 +132,7 @@ catch (e) {
|
|||
### acmeClient.createAccount([data]) ⇒ <code>Promise.<object></code>
|
||||
Create a new account
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
@ -161,7 +161,7 @@ const account = await client.createAccount({
|
|||
### acmeClient.updateAccount([data]) ⇒ <code>Promise.<object></code>
|
||||
Update existing account
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3.2
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
@ -182,7 +182,7 @@ const account = await client.updateAccount({
|
|||
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ <code>Promise.<object></code>
|
||||
Update account private key
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.3.5
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Account
|
||||
|
@ -203,7 +203,7 @@ const result = await client.updateAccountKey(newAccountKey);
|
|||
### acmeClient.createOrder(data) ⇒ <code>Promise.<object></code>
|
||||
Create a new order
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
@ -227,7 +227,7 @@ const order = await client.createOrder({
|
|||
### acmeClient.getOrder(order) ⇒ <code>Promise.<object></code>
|
||||
Refresh order object from CA
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
@ -246,7 +246,7 @@ const result = await client.getOrder(order);
|
|||
### acmeClient.finalizeOrder(order, csr) ⇒ <code>Promise.<object></code>
|
||||
Finalize order
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Order
|
||||
|
@ -268,7 +268,7 @@ const result = await client.finalizeOrder(order, csr);
|
|||
### acmeClient.getAuthorizations(order) ⇒ <code>Promise.<Array.<object>></code>
|
||||
Get identifier authorizations from order
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<Array.<object>></code> - Authorizations
|
||||
|
@ -292,7 +292,7 @@ authorizations.forEach((authz) => {
|
|||
### acmeClient.deactivateAuthorization(authz) ⇒ <code>Promise.<object></code>
|
||||
Deactivate identifier authorization
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5.2
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Authorization
|
||||
|
@ -312,7 +312,7 @@ const result = await client.deactivateAuthorization(authz);
|
|||
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ <code>Promise.<string></code>
|
||||
Get key authorization for ACME challenge
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-8.1
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<string></code> - Key authorization
|
||||
|
@ -353,7 +353,7 @@ await client.verifyChallenge(authz, challenge);
|
|||
### acmeClient.completeChallenge(challenge) ⇒ <code>Promise.<object></code>
|
||||
Notify CA that challenge has been completed
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5.1
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Challenge
|
||||
|
@ -373,7 +373,7 @@ const result = await client.completeChallenge(challenge);
|
|||
### acmeClient.waitForValidStatus(item) ⇒ <code>Promise.<object></code>
|
||||
Wait for ACME provider to verify status on a order, authorization or challenge
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.5.1
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<object></code> - Valid order, authorization or challenge
|
||||
|
@ -389,7 +389,7 @@ const challenge = { ... };
|
|||
await client.waitForValidStatus(challenge);
|
||||
```
|
||||
**Example**
|
||||
Wait for valid authoriation status
|
||||
Wait for valid authorization status
|
||||
```js
|
||||
const authz = { ... };
|
||||
await client.waitForValidStatus(authz);
|
||||
|
@ -405,7 +405,7 @@ await client.waitForValidStatus(order);
|
|||
### acmeClient.getCertificate(order, [preferredChain]) ⇒ <code>Promise.<string></code>
|
||||
Get certificate from ACME order
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.4.2
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
**Returns**: <code>Promise.<string></code> - Certificate
|
||||
|
@ -432,7 +432,7 @@ const certificate = await client.getCertificate(order, 'DST Root CA X3');
|
|||
### acmeClient.revokeCertificate(cert, [data]) ⇒ <code>Promise</code>
|
||||
Revoke certificate
|
||||
|
||||
https://tools.ietf.org/html/rfc8555#section-7.6
|
||||
https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
|
||||
|
||||
**Kind**: instance method of [<code>AcmeClient</code>](#AcmeClient)
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<dd><p>Get a JSON Web Key derived from a RSA or ECDSA key</p>
|
||||
<p><a href="https://datatracker.ietf.org/doc/html/rfc7517">https://datatracker.ietf.org/doc/html/rfc7517</a></p>
|
||||
</dd>
|
||||
<dt><a href="#splitPemChain">splitPemChain(chainPem)</a> ⇒ <code>array</code></dt>
|
||||
<dt><a href="#splitPemChain">splitPemChain(chainPem)</a> ⇒ <code>Array.<string></code></dt>
|
||||
<dd><p>Split chain of PEM encoded objects from string into array</p>
|
||||
</dd>
|
||||
<dt><a href="#getPemBodyAsB64u">getPemBodyAsB64u(pem)</a> ⇒ <code>string</code></dt>
|
||||
|
@ -42,6 +42,13 @@ If multiple certificates are chained, the first will be read</p>
|
|||
<dt><a href="#createCsr">createCsr(data, [keyPem])</a> ⇒ <code>Promise.<Array.<buffer>></code></dt>
|
||||
<dd><p>Create a Certificate Signing Request</p>
|
||||
</dd>
|
||||
<dt><a href="#createAlpnCertificate">createAlpnCertificate(authz, keyAuthorization, [keyPem])</a> ⇒ <code>Promise.<Array.<buffer>></code></dt>
|
||||
<dd><p>Create a self-signed ALPN certificate for TLS-ALPN-01 challenges</p>
|
||||
<p><a href="https://datatracker.ietf.org/doc/html/rfc8737">https://datatracker.ietf.org/doc/html/rfc8737</a></p>
|
||||
</dd>
|
||||
<dt><a href="#isAlpnCertificateAuthorizationValid">isAlpnCertificateAuthorizationValid(certPem, keyAuthorization)</a> ⇒ <code>boolean</code></dt>
|
||||
<dd><p>Validate that a ALPN certificate contains the expected key authorization</p>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<a name="crypto"></a>
|
||||
|
@ -138,11 +145,11 @@ const jwk = acme.crypto.getJwk(privateKey);
|
|||
```
|
||||
<a name="splitPemChain"></a>
|
||||
|
||||
## splitPemChain(chainPem) ⇒ <code>array</code>
|
||||
## splitPemChain(chainPem) ⇒ <code>Array.<string></code>
|
||||
Split chain of PEM encoded objects from string into array
|
||||
|
||||
**Kind**: global function
|
||||
**Returns**: <code>array</code> - Array of PEM objects including headers
|
||||
**Returns**: <code>Array.<string></code> - Array of PEM objects including headers
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
@ -219,14 +226,14 @@ Create a Certificate Signing Request
|
|||
| data | <code>object</code> | |
|
||||
| [data.keySize] | <code>number</code> | Size of newly created RSA private key modulus in bits, default: `2048` |
|
||||
| [data.commonName] | <code>string</code> | FQDN of your server |
|
||||
| [data.altNames] | <code>array</code> | SAN (Subject Alternative Names), default: `[]` |
|
||||
| [data.altNames] | <code>Array.<string></code> | SAN (Subject Alternative Names), default: `[]` |
|
||||
| [data.country] | <code>string</code> | 2 letter country code |
|
||||
| [data.state] | <code>string</code> | State or province |
|
||||
| [data.locality] | <code>string</code> | City |
|
||||
| [data.organization] | <code>string</code> | Organization name |
|
||||
| [data.organizationUnit] | <code>string</code> | Organizational unit name |
|
||||
| [data.emailAddress] | <code>string</code> | Email address |
|
||||
| [keyPem] | <code>string</code> | PEM encoded CSR private key |
|
||||
| [keyPem] | <code>buffer</code> \| <code>string</code> | PEM encoded CSR private key |
|
||||
|
||||
**Example**
|
||||
Create a Certificate Signing Request
|
||||
|
@ -265,3 +272,42 @@ const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
|||
const [, certificateRequest] = await acme.crypto.createCsr({
|
||||
commonName: 'test.example.com'
|
||||
}, certificateKey);
|
||||
<a name="createAlpnCertificate"></a>
|
||||
|
||||
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.<Array.<buffer>></code>
|
||||
Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
|
||||
|
||||
https://datatracker.ietf.org/doc/html/rfc8737
|
||||
|
||||
**Kind**: global function
|
||||
**Returns**: <code>Promise.<Array.<buffer>></code> - [privateKey, certificate]
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| authz | <code>object</code> | Identifier authorization |
|
||||
| keyAuthorization | <code>string</code> | Challenge key authorization |
|
||||
| [keyPem] | <code>buffer</code> \| <code>string</code> | PEM encoded CSR private key |
|
||||
|
||||
**Example**
|
||||
Create a ALPN certificate
|
||||
```js
|
||||
const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
|
||||
```
|
||||
**Example**
|
||||
Create a ALPN certificate with ECDSA private key
|
||||
```js
|
||||
const alpnKey = await acme.crypto.createPrivateEcdsaKey();
|
||||
const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
|
||||
<a name="isAlpnCertificateAuthorizationValid"></a>
|
||||
|
||||
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>
|
||||
Validate that a ALPN certificate contains the expected key authorization
|
||||
|
||||
**Kind**: global function
|
||||
**Returns**: <code>boolean</code> - True when valid
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| certPem | <code>buffer</code> \| <code>string</code> | PEM encoded certificate |
|
||||
| keyAuthorization | <code>string</code> | Expected challenge key authorization |
|
||||
|
||||
|
|
|
@ -209,7 +209,7 @@ Create a Certificate Signing Request
|
|||
| data | <code>object</code> | |
|
||||
| [data.keySize] | <code>number</code> | Size of newly created private key, default: `2048` |
|
||||
| [data.commonName] | <code>string</code> | |
|
||||
| [data.altNames] | <code>array</code> | default: `[]` |
|
||||
| [data.altNames] | <code>Array.<string></code> | default: `[]` |
|
||||
| [data.country] | <code>string</code> | |
|
||||
| [data.state] | <code>string</code> | |
|
||||
| [data.locality] | <code>string</code> | |
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# dns-01
|
||||
|
||||
The greatest benefit of `dns-01` is that it is the only challenge type that can be used to issue ACME wildcard certificates, however it also has a few downsides. Your DNS provider needs to offer some sort of API you can use to automate adding and removing the required `TXT` DNS records. Additionally, solving DNS challenges will be much slower than the other challenge types because of DNS propagation delays.
|
||||
|
||||
## How it works
|
||||
|
||||
When solving `dns-01` challenges, you prove ownership of a domain by serving a specific payload within a specific DNS `TXT` record from the domains authoritative nameservers. The ACME authority provides the client with a token that, along with a thumbprint of your account key, is used to generate a `base64url` encoded `SHA256` digest. This payload is then placed as a `TXT` record under DNS name `_acme-challenge.$YOUR_DOMAIN`.
|
||||
|
||||
Once the order is finalized, the ACME authority will lookup your domains DNS record to verify that the payload is correct. `CNAME` and `NS` records are followed, should you wish to delegate challenge response to another DNS zone or record.
|
||||
|
||||
## Pros and cons
|
||||
|
||||
* Only challenge type that can be used to issue wildcard certificates
|
||||
* Your DNS provider needs to supply an API that can be used
|
||||
* DNS propagation time may be slow
|
||||
* Useful in instances where both port 80 and 443 are unavailable
|
||||
|
||||
## External links
|
||||
|
||||
* [https://letsencrypt.org/docs/challenge-types/#dns-01-challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
|
||||
* [https://datatracker.ietf.org/doc/html/rfc8555#section-8.4](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4)
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Example using dns-01 challenge to generate certificates
|
||||
*
|
||||
* NOTE: This example is incomplete as the DNS challenge response implementation
|
||||
* will be specific to your DNS providers API.
|
||||
*
|
||||
* NOTE: This example does not order certificates on-demand, as solving dns-01
|
||||
* will likely be too slow for it to make sense. Instead, it orders a wildcard
|
||||
* certificate on init before starting the HTTPS server as a demonstration.
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const acme = require('./../../');
|
||||
|
||||
const HTTPS_SERVER_PORT = 443;
|
||||
const WILDCARD_DOMAIN = 'example.com';
|
||||
|
||||
function log(m) {
|
||||
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
/**
|
||||
* Initialize ACME client
|
||||
*/
|
||||
|
||||
log('Initializing ACME client');
|
||||
const client = new acme.Client({
|
||||
directoryUrl: acme.directory.letsencrypt.staging,
|
||||
accountKey: await acme.crypto.createPrivateKey()
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Order wildcard certificate
|
||||
*/
|
||||
|
||||
log(`Creating CSR for ${WILDCARD_DOMAIN}`);
|
||||
const [key, csr] = await acme.crypto.createCsr({
|
||||
commonName: WILDCARD_DOMAIN,
|
||||
altNames: [`*.${WILDCARD_DOMAIN}`]
|
||||
});
|
||||
|
||||
log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
|
||||
const cert = await client.auto({
|
||||
csr,
|
||||
email: 'test@example.com',
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ['dns-01'],
|
||||
challengeCreateFn: (authz, challenge, keyAuthorization) => {
|
||||
/* TODO: Implement this */
|
||||
log(`[TODO] Add TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
|
||||
},
|
||||
challengeRemoveFn: (authz, challenge, keyAuthorization) => {
|
||||
/* TODO: Implement this */
|
||||
log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
|
||||
}
|
||||
});
|
||||
|
||||
log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
|
||||
|
||||
|
||||
/**
|
||||
* HTTPS server
|
||||
*/
|
||||
|
||||
const requestListener = (req, res) => {
|
||||
log(`HTTP 200 ${req.headers.host}${req.url}`);
|
||||
res.writeHead(200);
|
||||
res.end('Hello world\n');
|
||||
};
|
||||
|
||||
const httpsServer = https.createServer({
|
||||
key,
|
||||
cert
|
||||
}, requestListener);
|
||||
|
||||
httpsServer.listen(HTTPS_SERVER_PORT, () => {
|
||||
log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
log(`[FATAL] ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
|
@ -2,7 +2,7 @@
|
|||
"name": "acme-client",
|
||||
"description": "Simple and unopinionated ACME client",
|
||||
"author": "nmorsman",
|
||||
"version": "5.2.0",
|
||||
"version": "5.3.0",
|
||||
"main": "src/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"license": "MIT",
|
||||
|
|
Loading…
Reference in New Issue