🔱: [acme] sync upgrade with 5 commits [trident-sync]

Update IETF links
Fix misc typos
Forgot SAN extension for self-signed ALPN certs
Replace jsrsasign dep with @peculiar/x509
pull/29/head
GitHub Actions Bot 2024-02-03 19:24:11 +00:00
parent a6bf198604
commit 80cd1bfc8e
13 changed files with 307 additions and 247 deletions

View File

@ -3,6 +3,7 @@
## v5.3.0 ## v5.3.0
* `added` Support and tests for satisfying `tls-alpn-01` challenges * `added` Support and tests for satisfying `tls-alpn-01` challenges
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge * `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
* This change is not considered breaking since the previous behavior was incorrect * This change is not considered breaking since the previous behavior was incorrect
@ -54,13 +55,13 @@
## v4.2.0 (2022-01-06) ## v4.2.0 (2022-01-06)
* `added` Support for external account binding - [RFC 8555 Section 7.3.4](https://tools.ietf.org/html/rfc8555#section-7.3.4) * `added` Support for external account binding - [RFC 8555 Section 7.3.4](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4)
* `added` Ability to pass through custom logger function * `added` Ability to pass through custom logger function
* `changed` Increase default `backoffAttempts` to 10 * `changed` Increase default `backoffAttempts` to 10
* `fixed` Deactivate authorizations where challenges can not be completed * `fixed` Deactivate authorizations where challenges can not be completed
* `fixed` Attempt authoritative name servers when verifying `dns-01` challenges * `fixed` Attempt authoritative name servers when verifying `dns-01` challenges
* `fixed` Error verbosity when failing to read ACME directory * `fixed` Error verbosity when failing to read ACME directory
* `fixed` Correctly recognize `ready` and `processing` states - [RFC 8555 Section 7.1.6](https://tools.ietf.org/html/rfc8555#section-7.1.6) * `fixed` Correctly recognize `ready` and `processing` states - [RFC 8555 Section 7.1.6](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6)
## v4.1.4 (2021-12-23) ## v4.1.4 (2021-12-23)
@ -110,7 +111,7 @@
## v3.3.0 (2019-12-19) ## v3.3.0 (2019-12-19)
* `added` TypeScript definitions * `added` TypeScript definitions
* `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://tools.ietf.org/html/rfc8555#section-7.1.1) * `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1)
## v3.2.1 (2019-11-14) ## v3.2.1 (2019-11-14)
@ -121,10 +122,10 @@
* `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble) * `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble)
* `changed` When creating a CSR, `commonName` no longer defaults to `'localhost'` * `changed` When creating a CSR, `commonName` no longer defaults to `'localhost'`
* This change is not considered breaking since `commonName: 'localhost'` will result in an error when ordering a certificate * This change is not considered breaking since `commonName: 'localhost'` will result in an error when ordering a certificate
* `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://tools.ietf.org/html/rfc8555#section-6.5) * `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5)
* `fixed` Minor bugs related to `POST-as-GET` when calling `updateAccount()` * `fixed` Minor bugs related to `POST-as-GET` when calling `updateAccount()`
* `fixed` Ensure subject common name is present in SAN when creating a CSR - [CAB v1.2.3 Section 9.2.2](https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf) * `fixed` Ensure subject common name is present in SAN when creating a CSR - [CAB v1.2.3 Section 9.2.2](https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf)
* `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://tools.ietf.org/html/rfc8555#section-7.5.1) * `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1)
## v2.3.1 (2019-08-26) ## v2.3.1 (2019-08-26)
@ -133,8 +134,8 @@
## v3.1.0 (2019-08-21) ## v3.1.0 (2019-08-21)
* `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://tools.ietf.org/html/rfc5280) * `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280)
* `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://tools.ietf.org/html/rfc8555#section-6.3) * `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://datatracker.ietf.org/doc/html/rfc8555#section-6.3)
## v2.3.0 (2019-08-21) ## v2.3.0 (2019-08-21)
@ -171,7 +172,7 @@
## v2.0.1 (2018-08-17) ## v2.0.1 (2018-08-17)
* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://tools.ietf.org/html/draft-ietf-acme-acme-13) * `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-13)
## v2.0.0 (2018-04-02) ## v2.0.0 (2018-04-02)

View File

@ -4,7 +4,7 @@
This module is written to handle communication with a Boulder/Let's Encrypt-style ACME API. This module is written to handle communication with a Boulder/Let's Encrypt-style ACME API.
* RFC 8555 - Automatic Certificate Management Environment (ACME): [https://tools.ietf.org/html/rfc8555](https://tools.ietf.org/html/rfc8555) * RFC 8555 - Automatic Certificate Management Environment (ACME): [https://datatracker.ietf.org/doc/html/rfc8555](https://datatracker.ietf.org/doc/html/rfc8555)
* Boulder divergences from ACME: [https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) * Boulder divergences from ACME: [https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md)
## Compatibility ## Compatibility
@ -67,7 +67,7 @@ acme.directory.zerossl.production;
### External account binding ### External account binding
To enable [external account binding](https://tools.ietf.org/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor. To enable [external account binding](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor.
```js ```js
const client = new acme.Client({ const client = new acme.Client({
@ -102,7 +102,7 @@ const myAccountUrl = client.getAccountUrl();
## Cryptography ## Cryptography
For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [jsrsasign](https://www.npmjs.com/package/jsrsasign) is used to generate and parse Certificate Signing Requests. For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) is used to generate and parse Certificate Signing Requests.
These utility methods are exposed through `.crypto`. These utility methods are exposed through `.crypto`.

View File

@ -6,7 +6,7 @@ First off this release drops support for Node LTS v10, v12 and v14, and the reas
## New native crypto interface ## New native crypto interface
A new crypto interface has been introduced with v5, which you can find under `acme.crypto`. It uses native Node.js cryptography APIs to generate private keys, JSON Web Keys and signatures, and finally enables support for ECC/ECDSA (P-256, P384 and P521), both for account private keys and certificates. The [jsrsasign](https://www.npmjs.com/package/jsrsasign) module is used to handle generation and parsing of Certificate Signing Requests. A new crypto interface has been introduced with v5, which you can find under `acme.crypto`. It uses native Node.js cryptography APIs to generate private keys, JSON Web Keys and signatures, and finally enables support for ECC/ECDSA (P-256, P384 and P521), both for account private keys and certificates. The [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) module is used to handle generation and parsing of Certificate Signing Requests.
Full documentation of `acme.crypto` can be [found here](crypto.md). Full documentation of `acme.crypto` can be [found here](crypto.md).

View File

@ -15,9 +15,10 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@peculiar/x509": "^1.9.7",
"asn1js": "^3.0.5",
"axios": "^1.6.5", "axios": "^1.6.5",
"debug": "^4.1.1", "debug": "^4.1.1",
"jsrsasign": "^11.0.0",
"node-forge": "^1.3.1" "node-forge": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -41,7 +41,7 @@ class AcmeApi {
* @private * @private
* @param {string} url Request URL * @param {string} url Request URL
* @param {object} [payload] Request payload, default: `null` * @param {object} [payload] Request payload, default: `null`
* @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts] * @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true` * @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false` * @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@ -66,7 +66,7 @@ class AcmeApi {
* @private * @private
* @param {string} resource Request resource name * @param {string} resource Request resource name
* @param {object} [payload] Request payload, default: `null` * @param {object} [payload] Request payload, default: `null`
* @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]` * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts] * @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true` * @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false` * @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@ -82,7 +82,7 @@ class AcmeApi {
/** /**
* Get Terms of Service URL if available * Get Terms of Service URL if available
* *
* https://tools.ietf.org/html/rfc8555#section-7.1.1 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
* *
* @returns {Promise<string|null>} ToS URL * @returns {Promise<string|null>} ToS URL
*/ */
@ -95,7 +95,7 @@ class AcmeApi {
/** /**
* Create new account * Create new account
* *
* https://tools.ietf.org/html/rfc8555#section-7.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* *
* @param {object} data Request payload * @param {object} data Request payload
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -119,7 +119,7 @@ class AcmeApi {
/** /**
* Update account * Update account
* *
* https://tools.ietf.org/html/rfc8555#section-7.3.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
* *
* @param {object} data Request payload * @param {object} data Request payload
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -133,7 +133,7 @@ class AcmeApi {
/** /**
* Update account key * Update account key
* *
* https://tools.ietf.org/html/rfc8555#section-7.3.5 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
* *
* @param {object} data Request payload * @param {object} data Request payload
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -147,7 +147,7 @@ class AcmeApi {
/** /**
* Create new order * Create new order
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {object} data Request payload * @param {object} data Request payload
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -161,7 +161,7 @@ class AcmeApi {
/** /**
* Get order * Get order
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {string} url Order URL * @param {string} url Order URL
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -175,7 +175,7 @@ class AcmeApi {
/** /**
* Finalize order * Finalize order
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {string} url Finalization URL * @param {string} url Finalization URL
* @param {object} data Request payload * @param {object} data Request payload
@ -190,7 +190,7 @@ class AcmeApi {
/** /**
* Get identifier authorization * Get identifier authorization
* *
* https://tools.ietf.org/html/rfc8555#section-7.5 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
* *
* @param {string} url Authorization URL * @param {string} url Authorization URL
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response
@ -204,7 +204,7 @@ class AcmeApi {
/** /**
* Update identifier authorization * Update identifier authorization
* *
* https://tools.ietf.org/html/rfc8555#section-7.5.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
* *
* @param {string} url Authorization URL * @param {string} url Authorization URL
* @param {object} data Request payload * @param {object} data Request payload
@ -219,7 +219,7 @@ class AcmeApi {
/** /**
* Complete challenge * Complete challenge
* *
* https://tools.ietf.org/html/rfc8555#section-7.5.1 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
* *
* @param {string} url Challenge URL * @param {string} url Challenge URL
* @param {object} data Request payload * @param {object} data Request payload
@ -234,7 +234,7 @@ class AcmeApi {
/** /**
* Revoke certificate * Revoke certificate
* *
* https://tools.ietf.org/html/rfc8555#section-7.6 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
* *
* @param {object} data Request payload * @param {object} data Request payload
* @returns {Promise<object>} HTTP response * @returns {Promise<object>} HTTP response

View File

@ -154,7 +154,7 @@ class AcmeClient {
/** /**
* Create a new account * Create a new account
* *
* https://tools.ietf.org/html/rfc8555#section-7.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* *
* @param {object} [data] Request data * @param {object} [data] Request data
* @returns {Promise<object>} Account * @returns {Promise<object>} Account
@ -200,7 +200,7 @@ class AcmeClient {
/** /**
* Update existing account * Update existing account
* *
* https://tools.ietf.org/html/rfc8555#section-7.3.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
* *
* @param {object} [data] Request data * @param {object} [data] Request data
* @returns {Promise<object>} Account * @returns {Promise<object>} Account
@ -240,7 +240,7 @@ class AcmeClient {
/** /**
* Update account private key * 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
* *
* @param {buffer|string} newAccountKey New PEM encoded private key * @param {buffer|string} newAccountKey New PEM encoded private key
* @param {object} [data] Additional request data * @param {object} [data] Additional request data
@ -286,7 +286,7 @@ class AcmeClient {
/** /**
* Create a new order * Create a new order
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {object} data Request data * @param {object} data Request data
* @returns {Promise<object>} Order * @returns {Promise<object>} Order
@ -318,7 +318,7 @@ class AcmeClient {
/** /**
* Refresh order object from CA * Refresh order object from CA
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {object} order Order object * @param {object} order Order object
* @returns {Promise<object>} Order * @returns {Promise<object>} Order
@ -345,7 +345,7 @@ class AcmeClient {
/** /**
* Finalize order * Finalize order
* *
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
* *
* @param {object} order Order object * @param {object} order Order object
* @param {buffer|string} csr PEM encoded Certificate Signing Request * @param {buffer|string} csr PEM encoded Certificate Signing Request
@ -380,7 +380,7 @@ class AcmeClient {
/** /**
* Get identifier authorizations from order * Get identifier authorizations from order
* *
* https://tools.ietf.org/html/rfc8555#section-7.5 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
* *
* @param {object} order Order * @param {object} order Order
* @returns {Promise<object[]>} Authorizations * @returns {Promise<object[]>} Authorizations
@ -410,7 +410,7 @@ class AcmeClient {
/** /**
* Deactivate identifier authorization * Deactivate identifier authorization
* *
* https://tools.ietf.org/html/rfc8555#section-7.5.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
* *
* @param {object} authz Identifier authorization * @param {object} authz Identifier authorization
* @returns {Promise<object>} Authorization * @returns {Promise<object>} Authorization
@ -442,7 +442,7 @@ class AcmeClient {
/** /**
* Get key authorization for ACME challenge * 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
* *
* @param {object} challenge Challenge object returned by API * @param {object} challenge Challenge object returned by API
* @returns {Promise<string>} Key authorization * @returns {Promise<string>} Key authorization
@ -462,17 +462,17 @@ class AcmeClient {
const thumbprint = keysum.digest('base64url'); const thumbprint = keysum.digest('base64url');
const result = `${challenge.token}.${thumbprint}`; const result = `${challenge.token}.${thumbprint}`;
/* https://tools.ietf.org/html/rfc8555#section-8.3 */ /* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 */
if (challenge.type === 'http-01') { if (challenge.type === 'http-01') {
return result; return result;
} }
/* https://tools.ietf.org/html/rfc8555#section-8.4 */ /* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 */
if (challenge.type === 'dns-01') { if (challenge.type === 'dns-01') {
return createHash('sha256').update(result).digest('base64url'); return createHash('sha256').update(result).digest('base64url');
} }
/* https://tools.ietf.org/html/rfc8737 */ /* https://datatracker.ietf.org/doc/html/rfc8737 */
if (challenge.type === 'tls-alpn-01') { if (challenge.type === 'tls-alpn-01') {
return result; return result;
} }
@ -519,7 +519,7 @@ class AcmeClient {
/** /**
* Notify CA that challenge has been completed * 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
* *
* @param {object} challenge Challenge object returned by API * @param {object} challenge Challenge object returned by API
* @returns {Promise<object>} Challenge * @returns {Promise<object>} Challenge
@ -540,7 +540,7 @@ class AcmeClient {
/** /**
* Wait for ACME provider to verify status on a order, authorization or challenge * 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
* *
* @param {object} item An order, authorization or challenge object * @param {object} item An order, authorization or challenge object
* @returns {Promise<object>} Valid order, authorization or challenge * @returns {Promise<object>} Valid order, authorization or challenge
@ -551,7 +551,7 @@ class AcmeClient {
* await client.waitForValidStatus(challenge); * await client.waitForValidStatus(challenge);
* ``` * ```
* *
* @example Wait for valid authoriation status * @example Wait for valid authorization status
* ```js * ```js
* const authz = { ... }; * const authz = { ... };
* await client.waitForValidStatus(authz); * await client.waitForValidStatus(authz);
@ -597,7 +597,7 @@ class AcmeClient {
/** /**
* Get certificate from ACME order * 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
* *
* @param {object} order Order object * @param {object} order Order object
* @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null` * @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
@ -644,7 +644,7 @@ class AcmeClient {
/** /**
* Revoke certificate * Revoke certificate
* *
* https://tools.ietf.org/html/rfc8555#section-7.6 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
* *
* @param {buffer|string} cert PEM encoded certificate * @param {buffer|string} cert PEM encoded certificate
* @param {object} [data] Additional request data * @param {object} [data] Additional request data

View File

@ -281,7 +281,7 @@ exports.readCertificateInfo = async function(cert) {
/** /**
* Determine ASN.1 type for CSR subject short name * Determine ASN.1 type for CSR subject short name
* Note: https://tools.ietf.org/html/rfc5280 * Note: https://datatracker.ietf.org/doc/html/rfc5280
* *
* @private * @private
* @param {string} shortName CSR subject short name * @param {string} shortName CSR subject short name
@ -343,7 +343,7 @@ function formatCsrAltNames(altNames) {
* @param {object} data * @param {object} data
* @param {number} [data.keySize] Size of newly created private key, default: `2048` * @param {number} [data.keySize] Size of newly created private key, default: `2048`
* @param {string} [data.commonName] * @param {string} [data.commonName]
* @param {array} [data.altNames] default: `[]` * @param {string[]} [data.altNames] default: `[]`
* @param {string} [data.country] * @param {string} [data.country]
* @param {string} [data.state] * @param {string} [data.state]
* @param {string} [data.locality] * @param {string} [data.locality]

View File

@ -7,12 +7,19 @@
const net = require('net'); const net = require('net');
const { promisify } = require('util'); const { promisify } = require('util');
const crypto = require('crypto'); const crypto = require('crypto');
const jsrsasign = require('jsrsasign'); const asn1js = require('asn1js');
const x509 = require('@peculiar/x509');
const randomInt = promisify(crypto.randomInt); const randomInt = promisify(crypto.randomInt);
const generateKeyPair = promisify(crypto.generateKeyPair); const generateKeyPair = promisify(crypto.generateKeyPair);
/* https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */ /* Use Node.js Web Crypto API */
x509.cryptoProvider.set(crypto.webcrypto);
/* id-ce-subjectAltName - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
const subjectAltNameOID = '2.5.29.17';
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31'; const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
@ -28,17 +35,14 @@ function getKeyInfo(keyPem) {
const result = { const result = {
isRSA: false, isRSA: false,
isECDSA: false, isECDSA: false,
signatureAlgorithm: null,
publicKey: crypto.createPublicKey(keyPem) publicKey: crypto.createPublicKey(keyPem)
}; };
if (result.publicKey.asymmetricKeyType === 'rsa') { if (result.publicKey.asymmetricKeyType === 'rsa') {
result.isRSA = true; result.isRSA = true;
result.signatureAlgorithm = 'SHA256withRSA';
} }
else if (result.publicKey.asymmetricKeyType === 'ec') { else if (result.publicKey.asymmetricKeyType === 'ec') {
result.isECDSA = true; result.isECDSA = true;
result.signatureAlgorithm = 'SHA256withECDSA';
} }
else { else {
throw new Error('Unable to parse key information, unknown format'); throw new Error('Unable to parse key information, unknown format');
@ -173,24 +177,42 @@ exports.getJwk = getJwk;
/** /**
* Fix missing support for NIST curve names in jsrsasign * Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
* *
* @private * @private
* @param {string} crv NIST curve name * @param {buffer|string} keyPem PEM encoded private key
* @returns {string} SECG curve name * @returns {Promise<array>} [keyPair, signingAlgorithm]
*/ */
function convertNistCurveNameToSecg(nistName) { async function getWebCryptoKeyPair(keyPem) {
switch (nistName) { const info = getKeyInfo(keyPem);
case 'P-256': const jwk = getJwk(keyPem);
return 'secp256r1';
case 'P-384': /* Signing algorithm */
return 'secp384r1'; const sigalg = {
case 'P-521': name: 'RSASSA-PKCS1-v1_5',
return 'secp521r1'; hash: { name: 'SHA-256' }
default: };
return nistName;
if (info.isECDSA) {
sigalg.name = 'ECDSA';
sigalg.namedCurve = jwk.crv;
if (jwk.crv === 'P-384') {
sigalg.hash.name = 'SHA-384';
}
if (jwk.crv === 'P-521') {
sigalg.hash.name = 'SHA-512';
}
} }
/* Decode PEM and import into CryptoKeyPair */
const privateKeyDec = x509.PemConverter.decodeFirst(keyPem.toString());
const privateKey = await crypto.webcrypto.subtle.importKey('pkcs8', privateKeyDec, sigalg, true, ['sign']);
const publicKey = await crypto.webcrypto.subtle.importKey('jwk', jwk, sigalg, true, ['verify']);
return [{ privateKey, publicKey }, sigalg];
} }
@ -198,7 +220,7 @@ function convertNistCurveNameToSecg(nistName) {
* Split chain of PEM encoded objects from string into array * Split chain of PEM encoded objects from string into array
* *
* @param {buffer|string} chainPem PEM encoded object chain * @param {buffer|string} chainPem PEM encoded object chain
* @returns {array} Array of PEM objects including headers * @returns {string[]} Array of PEM objects including headers
*/ */
function splitPemChain(chainPem) { function splitPemChain(chainPem) {
@ -206,15 +228,9 @@ function splitPemChain(chainPem) {
chainPem = chainPem.toString(); chainPem = chainPem.toString();
} }
return chainPem /* Decode into array and re-encode */
/* Split chain into chunks, starting at every header */ return x509.PemConverter.decodeWithHeaders(chainPem)
.split(/\s*(?=-----BEGIN [A-Z0-9- ]+-----\r?\n?)/g) .map((params) => x509.PemConverter.encode([params]));
/* Match header, PEM body and footer */
.map((pem) => pem.match(/\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\S\s]+)\r?\n?-----END \1-----/))
/* Filter out non-matches or empty bodies */
.filter((pem) => pem && pem[2] && pem[2].replace(/[\r\n]+/g, '').trim())
/* Decode to hex, and back to PEM for formatting etc */
.map(([pem, header]) => jsrsasign.hextopem(jsrsasign.pemtohex(pem, header), header));
} }
exports.splitPemChain = splitPemChain; exports.splitPemChain = splitPemChain;
@ -235,43 +251,28 @@ exports.getPemBodyAsB64u = (pem) => {
throw new Error('Unable to parse PEM body from string'); throw new Error('Unable to parse PEM body from string');
} }
/* Select first object, decode to hex and b64u */ /* Select first object, extract body and convert to b64u */
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0])); const dec = x509.PemConverter.decodeFirst(chain[0]);
return Buffer.from(dec).toString('base64url');
}; };
/**
* Parse common name from a subject object
*
* @private
* @param {object} subj Subject returned from jsrsasign
* @returns {string} Common name value
*/
function parseCommonName(subj) {
const subjectArr = (subj && subj.array) ? subj.array : [];
const cnArr = subjectArr.find((s) => (s[0] && s[0].type && s[0].value && (s[0].type === 'CN')));
return (cnArr && cnArr.length && cnArr[0].value) ? cnArr[0].value : null;
}
/** /**
* Parse domains from a certificate or CSR * Parse domains from a certificate or CSR
* *
* @private * @private
* @param {object} params Certificate or CSR params returned from jsrsasign * @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest
* @returns {object} {commonName, altNames} * @returns {object} {commonName, altNames}
*/ */
function parseDomains(params) { function parseDomains(input) {
const commonName = parseCommonName(params.subject); const commonName = input.subjectName.getField('CN').pop() || null;
const extensionArr = (params.ext || params.extreq || []); const altNamesRaw = input.getExtension(subjectAltNameOID);
let altNames = []; let altNames = [];
if (extensionArr && extensionArr.length) { if (altNamesRaw) {
const altNameExt = extensionArr.find((e) => (e.extname && (e.extname === 'subjectAltName'))); const altNamesExt = new x509.SubjectAlternativeNameExtension(altNamesRaw.rawData);
const altNameArr = (altNameExt && altNameExt.array && altNameExt.array.length) ? altNameExt.array : []; altNames = altNames.concat(altNamesExt.names.items.map((i) => i.value));
altNames = altNameArr.map((a) => Object.values(a)[0] || null).filter((a) => a);
} }
return { return {
@ -301,34 +302,12 @@ exports.readCsrDomains = (csrPem) => {
csrPem = csrPem.toString(); csrPem = csrPem.toString();
} }
/* Parse CSR */ const dec = x509.PemConverter.decodeFirst(csrPem);
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem); const csr = new x509.Pkcs10CertificateRequest(dec);
return parseDomains(params); return parseDomains(csr);
}; };
/**
* Parse params from a single or chain of PEM encoded certificates
*
* @private
* @param {buffer|string} certPem PEM encoded certificate or chain
* @returns {object} Certificate params
*/
function getCertificateParams(certPem) {
const chain = splitPemChain(certPem);
if (!chain.length) {
throw new Error('Unable to parse PEM body from string');
}
/* Parse certificate */
const obj = new jsrsasign.X509();
obj.readCertPEM(chain[0]);
return obj.getParam();
}
/** /**
* Read information from a certificate * Read information from a certificate
* If multiple certificates are chained, the first will be read * If multiple certificates are chained, the first will be read
@ -350,39 +329,43 @@ function getCertificateParams(certPem) {
*/ */
exports.readCertificateInfo = (certPem) => { exports.readCertificateInfo = (certPem) => {
const params = getCertificateParams(certPem); if (Buffer.isBuffer(certPem)) {
certPem = certPem.toString();
}
const dec = x509.PemConverter.decodeFirst(certPem);
const cert = new x509.X509Certificate(dec);
return { return {
issuer: { issuer: {
commonName: parseCommonName(params.issuer) commonName: cert.issuerName.getField('CN').pop() || null
}, },
domains: parseDomains(params), domains: parseDomains(cert),
notBefore: jsrsasign.zulutodate(params.notbefore), notBefore: cert.notBefore,
notAfter: jsrsasign.zulutodate(params.notafter) notAfter: cert.notAfter
}; };
}; };
/** /**
* Determine ASN.1 character string type for CSR subject field * Determine ASN.1 character string type for CSR subject field name
* *
* https://tools.ietf.org/html/rfc5280 * https://datatracker.ietf.org/doc/html/rfc5280
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/x509-1.1.js#L2404-L2412 * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/asn1x509-1.0.js#L3526-L3535
* *
* @private * @private
* @param {string} field CSR subject field * @param {string} field CSR subject field name
* @returns {string} ASN.1 jsrsasign character string type * @returns {string} ASN.1 character string type
*/ */
function getCsrAsn1CharStringType(field) { function getCsrAsn1CharStringType(field) {
switch (field) { switch (field) {
case 'C': case 'C':
return 'prn'; return 'printableString';
case 'E': case 'E':
return 'ia5'; return 'ia5String';
default: default:
return 'utf8'; return 'utf8String';
} }
} }
@ -390,6 +373,8 @@ function getCsrAsn1CharStringType(field) {
/** /**
* Create array of subject fields for a Certificate Signing Request * Create array of subject fields for a Certificate Signing Request
* *
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
*
* @private * @private
* @param {object} input Key-value of subject fields * @param {object} input Key-value of subject fields
* @returns {object[]} Certificate Signing Request subject array * @returns {object[]} Certificate Signing Request subject array
@ -399,7 +384,7 @@ function createCsrSubject(input) {
return Object.entries(input).reduce((result, [type, value]) => { return Object.entries(input).reduce((result, [type, value]) => {
if (value) { if (value) {
const ds = getCsrAsn1CharStringType(type); const ds = getCsrAsn1CharStringType(type);
result.push([{ type, value, ds }]); result.push({ [type]: [{ [ds]: value }] });
} }
return result; return result;
@ -408,20 +393,20 @@ function createCsrSubject(input) {
/** /**
* Create array of alt names for Certificate Signing Requests * Create x509 subject alternate name extension
* *
* https://github.com/kjur/jsrsasign/blob/3edc0070846922daea98d9588978e91d855577ec/src/x509-1.1.js#L1355-L1410 * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/extensions/subject_alt_name.ts
* *
* @private * @private
* @param {string[]} altNames Array of alt names * @param {string[]} altNames Array of alt names
* @returns {object[]} Certificate Signing Request alt names array * @returns {x509.SubjectAlternativeNameExtension} Subject alternate name extension
*/ */
function formatCsrAltNames(altNames) { function createSubjectAltNameExtension(altNames) {
return altNames.map((value) => { return new x509.SubjectAlternativeNameExtension(altNames.map((value) => {
const key = net.isIP(value) ? 'ip' : 'dns'; const type = net.isIP(value) ? 'ip' : 'dns';
return { [key]: value }; return { type, value };
}); }));
} }
@ -431,14 +416,14 @@ function formatCsrAltNames(altNames) {
* @param {object} data * @param {object} data
* @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048` * @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048`
* @param {string} [data.commonName] FQDN of your server * @param {string} [data.commonName] FQDN of your server
* @param {array} [data.altNames] SAN (Subject Alternative Names), default: `[]` * @param {string[]} [data.altNames] SAN (Subject Alternative Names), default: `[]`
* @param {string} [data.country] 2 letter country code * @param {string} [data.country] 2 letter country code
* @param {string} [data.state] State or province * @param {string} [data.state] State or province
* @param {string} [data.locality] City * @param {string} [data.locality] City
* @param {string} [data.organization] Organization name * @param {string} [data.organization] Organization name
* @param {string} [data.organizationUnit] Organizational unit name * @param {string} [data.organizationUnit] Organizational unit name
* @param {string} [data.emailAddress] Email address * @param {string} [data.emailAddress] Email address
* @param {string} [keyPem] PEM encoded CSR private key * @param {buffer|string} [keyPem] PEM encoded CSR private key
* @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest] * @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest]
* *
* @example Create a Certificate Signing Request * @example Create a Certificate Signing Request
@ -479,7 +464,7 @@ function formatCsrAltNames(altNames) {
* }, certificateKey); * }, certificateKey);
*/ */
async function createCsr(data, keyPem = null) { exports.createCsr = async (data, keyPem = null) => {
if (!keyPem) { if (!keyPem) {
keyPem = await createPrivateRsaKey(data.keySize); keyPem = await createPrivateRsaKey(data.keySize);
} }
@ -491,65 +476,52 @@ async function createCsr(data, keyPem = null) {
data.altNames = []; data.altNames = [];
} }
/* Get key info and JWK */
const info = getKeyInfo(keyPem);
const jwk = getJwk(keyPem);
const extensionRequests = [];
/* Missing support for NIST curve names in jsrsasign - https://github.com/kjur/jsrsasign/blob/master/src/asn1x509-1.0.js#L4388-L4393 */
if (jwk.crv && (jwk.kty === 'EC')) {
jwk.crv = convertNistCurveNameToSecg(jwk.crv);
}
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */ /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
if (data.commonName && !data.altNames.includes(data.commonName)) { if (data.commonName && !data.altNames.includes(data.commonName)) {
data.altNames.unshift(data.commonName); data.altNames.unshift(data.commonName);
} }
/* Subject */ /* CryptoKeyPair and signing algorithm from private key */
const subject = createCsrSubject({ const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
CN: data.commonName,
C: data.country,
ST: data.state,
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
});
/* SAN extension */ const extensions = [
if (data.altNames.length) { /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
extensionRequests.push({ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
extname: 'subjectAltName',
array: formatCsrAltNames(data.altNames) /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
}); createSubjectAltNameExtension(data.altNames)
} ];
/* Create CSR */ /* Create CSR */
const csr = new jsrsasign.KJUR.asn1.csr.CertificationRequest({ const csr = await x509.Pkcs10CertificateRequestGenerator.create({
subject: { array: subject }, keys,
sigalg: info.signatureAlgorithm, extensions,
sbjprvkey: keyPem.toString(), signingAlgorithm,
sbjpubkey: jwk, name: createCsrSubject({
extreq: extensionRequests CN: data.commonName,
C: data.country,
ST: data.state,
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
})
}); });
/* Done */ /* Done */
const pem = csr.getPEM(); const pem = csr.toString('pem');
return [keyPem, Buffer.from(pem)]; return [keyPem, Buffer.from(pem)];
} };
exports.createCsr = createCsr;
/** /**
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges * Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
* *
* https://tools.ietf.org/html/rfc8737 * https://datatracker.ietf.org/doc/html/rfc8737
* *
* @param {object} authz Identifier authorization * @param {object} authz Identifier authorization
* @param {string} keyAuthorization Challenge key authorization * @param {string} keyAuthorization Challenge key authorization
* @param {string} [keyPem] PEM encoded CSR private key * @param {buffer|string} [keyPem] PEM encoded CSR private key
* @returns {Promise<buffer[]>} [privateKey, certificate] * @returns {Promise<buffer[]>} [privateKey, certificate]
* *
* @example Create a ALPN certificate * @example Create a ALPN certificate
@ -564,45 +536,58 @@ exports.createCsr = createCsr;
*/ */
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => { exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
/* Create CSR first */ if (!keyPem) {
keyPem = await createPrivateRsaKey();
}
else if (!Buffer.isBuffer(keyPem)) {
keyPem = Buffer.from(keyPem);
}
const now = new Date(); const now = new Date();
const commonName = authz.identifier.value; const commonName = authz.identifier.value;
const [key, csr] = await createCsr({ commonName }, keyPem);
/* Parse params and grab stuff we need */
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csr.toString());
const { subject, sbjpubkey, extreq, sigalg } = params;
/* ALPN extension */
const alpnExt = {
critical: true,
extname: alpnAcmeIdentifierOID,
extn: new jsrsasign.KJUR.asn1.DEROctetString({
hex: crypto.createHash('sha256').update(keyAuthorization).digest('hex')
})
};
/* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */ /* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
const random = await randomInt(1, 999999999); const random = await randomInt(1, 999999999);
const serial = `${Math.floor(now.getTime() / 1000)}${random}`; const serialNumber = `${Math.floor(now.getTime() / 1000)}${random}`;
/* CryptoKeyPair and signing algorithm from private key */
const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
const extensions = [
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), // eslint-disable-line no-bitwise
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 */
new x509.BasicConstraintsExtension(true, 2, true),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 */
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension([commonName])
];
/* ALPN extension */
const payload = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
const octstr = new asn1js.OctetString({ valueHex: Buffer.from(payload, 'hex') });
extensions.push(new x509.Extension(alpnAcmeIdentifierOID, true, octstr.toBER()));
/* Self-signed ALPN certificate */ /* Self-signed ALPN certificate */
const certificate = new jsrsasign.KJUR.asn1.x509.Certificate({ const cert = await x509.X509CertificateGenerator.createSelfSigned({
subject, keys,
sbjpubkey, signingAlgorithm,
sigalg, extensions,
version: 3, serialNumber,
serial: { hex: Buffer.from(serial).toString('hex') }, notBefore: now,
issuer: subject, notAfter: now,
notbefore: jsrsasign.datetozulu(now), name: createCsrSubject({
notafter: jsrsasign.datetozulu(now), CN: commonName
cakey: key.toString(), })
ext: extreq.concat([alpnExt])
}); });
/* Done */ /* Done */
const pem = certificate.getPEM(); const pem = cert.toString('pem');
return [key, Buffer.from(pem)]; return [keyPem, Buffer.from(pem)];
}; };
@ -615,14 +600,20 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
*/ */
exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => { exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
const params = getCertificateParams(certPem); const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
const expectedHex = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
const acmeExt = (params.ext || []).find((e) => (e && e.extname && (e.extname === alpnAcmeIdentifierOID)));
if (!acmeExt || !acmeExt.extn || !acmeExt.extn.octstr || !acmeExt.extn.octstr.hex) { /* Attempt to locate ALPN extension */
const cert = new x509.X509Certificate(certPem);
const ext = cert.getExtension(alpnAcmeIdentifierOID);
if (!ext) {
throw new Error('Unable to locate ALPN extension within parsed certificate'); throw new Error('Unable to locate ALPN extension within parsed certificate');
} }
/* Decode extension value */
const parsed = asn1js.fromBER(ext.value);
const result = Buffer.from(parsed.result.valueBlock.valueHexView).toString('hex');
/* Return true if match */ /* Return true if match */
return (acmeExt.extn.octstr.hex === expectedHex); return (result === expected);
}; };

View File

@ -64,7 +64,7 @@ class HttpClient {
/** /**
* Ensure provider directory exists * Ensure provider directory exists
* *
* https://tools.ietf.org/html/rfc8555#section-7.1.1 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
* *
* @returns {Promise} * @returns {Promise}
*/ */
@ -104,7 +104,7 @@ class HttpClient {
/** /**
* Get nonce from directory API endpoint * Get nonce from directory API endpoint
* *
* https://tools.ietf.org/html/rfc8555#section-7.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
* *
* @returns {Promise<string>} nonce * @returns {Promise<string>} nonce
*/ */
@ -267,7 +267,7 @@ class HttpClient {
/** /**
* Signed HTTP request * Signed HTTP request
* *
* https://tools.ietf.org/html/rfc8555#section-6.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
* *
* @param {string} url Request URL * @param {string} url Request URL
* @param {object} payload Request payload * @param {object} payload Request payload
@ -299,7 +299,7 @@ class HttpClient {
const data = this.createSignedBody(url, payload, { nonce, kid }); const data = this.createSignedBody(url, payload, { nonce, kid });
const resp = await this.request(url, 'post', { data }); const resp = await this.request(url, 'post', { data });
/* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */ /* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) { if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
nonce = resp.headers['replay-nonce'] || null; nonce = resp.headers['replay-nonce'] || null;
attempts += 1; attempts += 1;

View File

@ -93,7 +93,7 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
* *
* @param {string} header Link header contents * @param {string} header Link header contents
* @param {string} rel Link relation, default: `alternate` * @param {string} rel Link relation, default: `alternate`
* @returns {array} Array of URLs * @returns {string[]} Array of URLs
*/ */
function parseLinkHeader(header, rel = 'alternate') { function parseLinkHeader(header, rel = 'alternate') {
@ -113,7 +113,7 @@ function parseLinkHeader(header, rel = 'alternate') {
* - If issuer is found in multiple chains, the closest to root wins * - If issuer is found in multiple chains, the closest to root wins
* - If issuer can not be located, the first chain will be returned * - If issuer can not be located, the first chain will be returned
* *
* @param {array} certificates Array of PEM encoded certificate chains * @param {string[]} certificates Array of PEM encoded certificate chains
* @param {string} issuer Preferred certificate issuer * @param {string} issuer Preferred certificate issuer
* @returns {string} PEM encoded certificate chain * @returns {string} PEM encoded certificate chain
*/ */

View File

@ -13,7 +13,7 @@ const { isAlpnCertificateAuthorizationValid } = require('./crypto');
/** /**
* Verify ACME HTTP challenge * Verify ACME HTTP challenge
* *
* https://tools.ietf.org/html/rfc8555#section-8.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
* *
* @param {object} authz Identifier authorization * @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge * @param {object} challenge Authorization challenge
@ -85,7 +85,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
/** /**
* Verify ACME DNS challenge * Verify ACME DNS challenge
* *
* https://tools.ietf.org/html/rfc8555#section-8.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
* *
* @param {object} authz Identifier authorization * @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge * @param {object} challenge Authorization challenge
@ -125,7 +125,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
/** /**
* Verify ACME TLS ALPN challenge * Verify ACME TLS ALPN challenge
* *
* https://tools.ietf.org/html/rfc8737 * https://datatracker.ietf.org/doc/html/rfc8737
* *
* @param {object} authz Identifier authorization * @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge * @param {object} challenge Authorization challenge

View File

@ -10,10 +10,10 @@ const { crypto } = require('./../');
const emptyBodyChain1 = ` const emptyBodyChain1 = `
-----BEGIN TEST----- -----BEGIN TEST-----
a dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
-----BEGIN TEST----- -----BEGIN TEST-----
b dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
-----BEGIN TEST----- -----BEGIN TEST-----
@ -22,7 +22,7 @@ b
-----BEGIN TEST----- -----BEGIN TEST-----
c dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
`; `;
@ -38,15 +38,15 @@ const emptyBodyChain2 = `
-----END TEST----- -----END TEST-----
-----BEGIN TEST----- -----BEGIN TEST-----
a dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
-----BEGIN TEST----- -----BEGIN TEST-----
b dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
-----BEGIN TEST----- -----BEGIN TEST-----
c dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST----- -----END TEST-----
`; `;
@ -112,6 +112,11 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(testPublicKeys[n])); assert.isTrue(Buffer.isBuffer(testPublicKeys[n]));
}); });
it(`${n}/should get public key from string`, () => {
testPublicKeys[n] = crypto.getPublicKey(testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(testPublicKeys[n]));
});
it(`${n}/should get jwk from private key`, () => { it(`${n}/should get jwk from private key`, () => {
const jwk = crypto.getJwk(testPrivateKeys[n]); const jwk = crypto.getJwk(testPrivateKeys[n]);
jwkSpecFn(jwk); jwkSpecFn(jwk);
@ -122,6 +127,11 @@ describe('crypto', () => {
jwkSpecFn(jwk); jwkSpecFn(jwk);
}); });
it(`${n}/should get jwk from string`, () => {
const jwk = crypto.getJwk(testPrivateKeys[n].toString());
jwkSpecFn(jwk);
});
/** /**
* Certificate Signing Request * Certificate Signing Request
@ -174,6 +184,15 @@ describe('crypto', () => {
testNonAsciiCsr = csr; testNonAsciiCsr = csr;
}); });
it(`${n}/should generate a csr with key as string`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
}, testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
});
it(`${n}/should throw with invalid key`, async () => { it(`${n}/should throw with invalid key`, async () => {
await assert.isRejected(crypto.createCsr({ await assert.isRejected(crypto.createCsr({
commonName: testCsrDomain commonName: testCsrDomain
@ -217,6 +236,13 @@ describe('crypto', () => {
assert.deepStrictEqual(result.altNames, [testCsrDomain]); assert.deepStrictEqual(result.altNames, [testCsrDomain]);
}); });
it(`${n}/should resolve domains from csr string`, () => {
[testCsr, testSanCsr, testNonCnCsr, testNonAsciiCsr].forEach((csr) => {
const result = crypto.readCsrDomains(csr.toString());
spec.crypto.csrDomains(result);
});
});
/** /**
* ALPN * ALPN
@ -232,6 +258,15 @@ describe('crypto', () => {
testAlpnCertificate = cert; testAlpnCertificate = cert;
}); });
it(`${n}/should generate alpn certificate with key as string`, async () => {
const k = await createFn();
const authz = { identifier: { value: 'test.example.com' } };
const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', k.toString());
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(cert));
});
it(`${n}/should not validate invalid alpn certificate key authorization`, () => { it(`${n}/should not validate invalid alpn certificate key authorization`, () => {
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa')); assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa'));
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb')); assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb'));
@ -241,6 +276,10 @@ describe('crypto', () => {
it(`${n}/should validate valid alpn certificate key authorization`, () => { it(`${n}/should validate valid alpn certificate key authorization`, () => {
assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'super-secret.12345')); assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'super-secret.12345'));
}); });
it(`${n}/should validate valid alpn certificate with cert as string`, () => {
assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate.toString(), 'super-secret.12345'));
});
}); });
}); });
}); });
@ -306,6 +345,13 @@ describe('crypto', () => {
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length)); assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
}); });
it('should read certificate info from string', () => {
[testCert, testSanCert].forEach((cert) => {
const info = crypto.readCertificateInfo(cert.toString());
spec.crypto.certificateInfo(info);
});
});
/** /**
* ALPN * ALPN
@ -335,6 +381,17 @@ describe('crypto', () => {
}); });
}); });
it('should get pem body as b64u from string', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const body = crypto.getPemBodyAsB64u(pem.toString());
assert.isString(body);
assert.notInclude(body, '\r');
assert.notInclude(body, '\n');
assert.notInclude(body, '\r\n');
});
});
it('should split pem chain', () => { it('should split pem chain', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => { [testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = crypto.splitPemChain(pem); const chain = crypto.splitPemChain(pem);
@ -345,6 +402,16 @@ describe('crypto', () => {
}); });
}); });
it('should split pem chain from string', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = crypto.splitPemChain(pem.toString());
assert.isArray(chain);
assert.isNotEmpty(chain);
chain.forEach((c) => assert.isString(c));
});
});
it('should split pem chain with empty bodies', () => { it('should split pem chain with empty bodies', () => {
const c1 = crypto.splitPemChain(emptyBodyChain1); const c1 = crypto.splitPemChain(emptyBodyChain1);
const c2 = crypto.splitPemChain(emptyBodyChain2); const c2 = crypto.splitPemChain(emptyBodyChain2);

View File

@ -1,9 +1,9 @@
/** /**
* Account * Account
* *
* https://tools.ietf.org/html/rfc8555#section-7.1.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2
* https://tools.ietf.org/html/rfc8555#section-7.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* https://tools.ietf.org/html/rfc8555#section-7.3.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*/ */
export interface Account { export interface Account {
@ -31,8 +31,8 @@ export interface AccountUpdateRequest {
/** /**
* Order * Order
* *
* https://tools.ietf.org/html/rfc8555#section-7.1.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3
* https://tools.ietf.org/html/rfc8555#section-7.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*/ */
export interface Order { export interface Order {
@ -57,7 +57,7 @@ export interface OrderCreateRequest {
/** /**
* Authorization * Authorization
* *
* https://tools.ietf.org/html/rfc8555#section-7.1.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4
*/ */
export interface Authorization { export interface Authorization {
@ -77,9 +77,9 @@ export interface Identifier {
/** /**
* Challenge * Challenge
* *
* https://tools.ietf.org/html/rfc8555#section-8 * https://datatracker.ietf.org/doc/html/rfc8555#section-8
* https://tools.ietf.org/html/rfc8555#section-8.3 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
* https://tools.ietf.org/html/rfc8555#section-8.4 * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*/ */
export interface ChallengeAbstract { export interface ChallengeAbstract {
@ -106,7 +106,7 @@ export type Challenge = HttpChallenge | DnsChallenge;
/** /**
* Certificate * Certificate
* *
* https://tools.ietf.org/html/rfc8555#section-7.6 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*/ */
export enum CertificateRevocationReason { export enum CertificateRevocationReason {