mirror of https://github.com/certd/certd
🔱: [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/x509pull/29/head
parent
a6bf198604
commit
80cd1bfc8e
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
||||||
|
|
|
@ -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).
|
||||||
|
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue