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
|
||||
|
||||
* `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
|
||||
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
|
||||
* This change is not considered breaking since the previous behavior was incorrect
|
||||
|
@ -54,13 +55,13 @@
|
|||
|
||||
## 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
|
||||
* `changed` Increase default `backoffAttempts` to 10
|
||||
* `fixed` Deactivate authorizations where challenges can not be completed
|
||||
* `fixed` Attempt authoritative name servers when verifying `dns-01` challenges
|
||||
* `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)
|
||||
|
||||
|
@ -110,7 +111,7 @@
|
|||
## v3.3.0 (2019-12-19)
|
||||
|
||||
* `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)
|
||||
|
||||
|
@ -121,10 +122,10 @@
|
|||
* `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble)
|
||||
* `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
|
||||
* `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` 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)
|
||||
|
||||
|
@ -133,8 +134,8 @@
|
|||
|
||||
## 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)
|
||||
* `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://tools.ietf.org/html/rfc8555#section-6.3)
|
||||
* `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://datatracker.ietf.org/doc/html/rfc8555#section-6.3)
|
||||
|
||||
## v2.3.0 (2019-08-21)
|
||||
|
||||
|
@ -171,7 +172,7 @@
|
|||
|
||||
## 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)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
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)
|
||||
|
||||
## Compatibility
|
||||
|
@ -67,7 +67,7 @@ acme.directory.zerossl.production;
|
|||
|
||||
### 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
|
||||
const client = new acme.Client({
|
||||
|
@ -102,7 +102,7 @@ const myAccountUrl = client.getAccountUrl();
|
|||
|
||||
## 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`.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ First off this release drops support for Node LTS v10, v12 and v14, and the reas
|
|||
|
||||
## 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).
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
"types"
|
||||
],
|
||||
"dependencies": {
|
||||
"@peculiar/x509": "^1.9.7",
|
||||
"asn1js": "^3.0.5",
|
||||
"axios": "^1.6.5",
|
||||
"debug": "^4.1.1",
|
||||
"jsrsasign": "^11.0.0",
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -41,7 +41,7 @@ class AcmeApi {
|
|||
* @private
|
||||
* @param {string} url Request URL
|
||||
* @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 {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
|
||||
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
|
||||
|
@ -66,7 +66,7 @@ class AcmeApi {
|
|||
* @private
|
||||
* @param {string} resource Request resource name
|
||||
* @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 {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
|
||||
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
|
||||
|
@ -82,7 +82,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -95,7 +95,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -119,7 +119,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -133,7 +133,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -147,7 +147,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -161,7 +161,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -175,7 +175,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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 {object} data Request payload
|
||||
|
@ -190,7 +190,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
@ -204,7 +204,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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 {object} data Request payload
|
||||
|
@ -219,7 +219,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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 {object} data Request payload
|
||||
|
@ -234,7 +234,7 @@ class AcmeApi {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} HTTP response
|
||||
|
|
|
@ -154,7 +154,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Account
|
||||
|
@ -200,7 +200,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Account
|
||||
|
@ -240,7 +240,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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 {object} [data] Additional request data
|
||||
|
@ -286,7 +286,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Order
|
||||
|
@ -318,7 +318,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Order
|
||||
|
@ -345,7 +345,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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 {buffer|string} csr PEM encoded Certificate Signing Request
|
||||
|
@ -380,7 +380,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object[]>} Authorizations
|
||||
|
@ -410,7 +410,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Authorization
|
||||
|
@ -442,7 +442,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<string>} Key authorization
|
||||
|
@ -462,17 +462,17 @@ class AcmeClient {
|
|||
const thumbprint = keysum.digest('base64url');
|
||||
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') {
|
||||
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') {
|
||||
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') {
|
||||
return result;
|
||||
}
|
||||
|
@ -519,7 +519,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Challenge
|
||||
|
@ -540,7 +540,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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
|
||||
* @returns {Promise<object>} Valid order, authorization or challenge
|
||||
|
@ -551,7 +551,7 @@ class AcmeClient {
|
|||
* await client.waitForValidStatus(challenge);
|
||||
* ```
|
||||
*
|
||||
* @example Wait for valid authoriation status
|
||||
* @example Wait for valid authorization status
|
||||
* ```js
|
||||
* const authz = { ... };
|
||||
* await client.waitForValidStatus(authz);
|
||||
|
@ -597,7 +597,7 @@ class AcmeClient {
|
|||
/**
|
||||
* 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 {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
|
||||
*
|
||||
* 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 {object} [data] Additional request data
|
||||
|
|
|
@ -281,7 +281,7 @@ exports.readCertificateInfo = async function(cert) {
|
|||
|
||||
/**
|
||||
* 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
|
||||
* @param {string} shortName CSR subject short name
|
||||
|
@ -343,7 +343,7 @@ function formatCsrAltNames(altNames) {
|
|||
* @param {object} data
|
||||
* @param {number} [data.keySize] Size of newly created private key, default: `2048`
|
||||
* @param {string} [data.commonName]
|
||||
* @param {array} [data.altNames] default: `[]`
|
||||
* @param {string[]} [data.altNames] default: `[]`
|
||||
* @param {string} [data.country]
|
||||
* @param {string} [data.state]
|
||||
* @param {string} [data.locality]
|
||||
|
|
|
@ -7,12 +7,19 @@
|
|||
const net = require('net');
|
||||
const { promisify } = require('util');
|
||||
const crypto = require('crypto');
|
||||
const jsrsasign = require('jsrsasign');
|
||||
const asn1js = require('asn1js');
|
||||
const x509 = require('@peculiar/x509');
|
||||
|
||||
const randomInt = promisify(crypto.randomInt);
|
||||
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';
|
||||
|
||||
|
||||
|
@ -28,17 +35,14 @@ function getKeyInfo(keyPem) {
|
|||
const result = {
|
||||
isRSA: false,
|
||||
isECDSA: false,
|
||||
signatureAlgorithm: null,
|
||||
publicKey: crypto.createPublicKey(keyPem)
|
||||
};
|
||||
|
||||
if (result.publicKey.asymmetricKeyType === 'rsa') {
|
||||
result.isRSA = true;
|
||||
result.signatureAlgorithm = 'SHA256withRSA';
|
||||
}
|
||||
else if (result.publicKey.asymmetricKeyType === 'ec') {
|
||||
result.isECDSA = true;
|
||||
result.signatureAlgorithm = 'SHA256withECDSA';
|
||||
}
|
||||
else {
|
||||
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
|
||||
* @param {string} crv NIST curve name
|
||||
* @returns {string} SECG curve name
|
||||
* @param {buffer|string} keyPem PEM encoded private key
|
||||
* @returns {Promise<array>} [keyPair, signingAlgorithm]
|
||||
*/
|
||||
|
||||
function convertNistCurveNameToSecg(nistName) {
|
||||
switch (nistName) {
|
||||
case 'P-256':
|
||||
return 'secp256r1';
|
||||
case 'P-384':
|
||||
return 'secp384r1';
|
||||
case 'P-521':
|
||||
return 'secp521r1';
|
||||
default:
|
||||
return nistName;
|
||||
async function getWebCryptoKeyPair(keyPem) {
|
||||
const info = getKeyInfo(keyPem);
|
||||
const jwk = getJwk(keyPem);
|
||||
|
||||
/* Signing algorithm */
|
||||
const sigalg = {
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
hash: { name: 'SHA-256' }
|
||||
};
|
||||
|
||||
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
|
||||
*
|
||||
* @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) {
|
||||
|
@ -206,15 +228,9 @@ function splitPemChain(chainPem) {
|
|||
chainPem = chainPem.toString();
|
||||
}
|
||||
|
||||
return chainPem
|
||||
/* Split chain into chunks, starting at every header */
|
||||
.split(/\s*(?=-----BEGIN [A-Z0-9- ]+-----\r?\n?)/g)
|
||||
/* 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));
|
||||
/* Decode into array and re-encode */
|
||||
return x509.PemConverter.decodeWithHeaders(chainPem)
|
||||
.map((params) => x509.PemConverter.encode([params]));
|
||||
}
|
||||
|
||||
exports.splitPemChain = splitPemChain;
|
||||
|
@ -235,43 +251,28 @@ exports.getPemBodyAsB64u = (pem) => {
|
|||
throw new Error('Unable to parse PEM body from string');
|
||||
}
|
||||
|
||||
/* Select first object, decode to hex and b64u */
|
||||
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
|
||||
/* Select first object, extract body and convert to b64u */
|
||||
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
|
||||
*
|
||||
* @private
|
||||
* @param {object} params Certificate or CSR params returned from jsrsasign
|
||||
* @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest
|
||||
* @returns {object} {commonName, altNames}
|
||||
*/
|
||||
|
||||
function parseDomains(params) {
|
||||
const commonName = parseCommonName(params.subject);
|
||||
const extensionArr = (params.ext || params.extreq || []);
|
||||
function parseDomains(input) {
|
||||
const commonName = input.subjectName.getField('CN').pop() || null;
|
||||
const altNamesRaw = input.getExtension(subjectAltNameOID);
|
||||
let altNames = [];
|
||||
|
||||
if (extensionArr && extensionArr.length) {
|
||||
const altNameExt = extensionArr.find((e) => (e.extname && (e.extname === 'subjectAltName')));
|
||||
const altNameArr = (altNameExt && altNameExt.array && altNameExt.array.length) ? altNameExt.array : [];
|
||||
altNames = altNameArr.map((a) => Object.values(a)[0] || null).filter((a) => a);
|
||||
if (altNamesRaw) {
|
||||
const altNamesExt = new x509.SubjectAlternativeNameExtension(altNamesRaw.rawData);
|
||||
altNames = altNames.concat(altNamesExt.names.items.map((i) => i.value));
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -301,34 +302,12 @@ exports.readCsrDomains = (csrPem) => {
|
|||
csrPem = csrPem.toString();
|
||||
}
|
||||
|
||||
/* Parse CSR */
|
||||
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
|
||||
return parseDomains(params);
|
||||
const dec = x509.PemConverter.decodeFirst(csrPem);
|
||||
const csr = new x509.Pkcs10CertificateRequest(dec);
|
||||
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
|
||||
* If multiple certificates are chained, the first will be read
|
||||
|
@ -350,39 +329,43 @@ function getCertificateParams(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 {
|
||||
issuer: {
|
||||
commonName: parseCommonName(params.issuer)
|
||||
commonName: cert.issuerName.getField('CN').pop() || null
|
||||
},
|
||||
domains: parseDomains(params),
|
||||
notBefore: jsrsasign.zulutodate(params.notbefore),
|
||||
notAfter: jsrsasign.zulutodate(params.notafter)
|
||||
domains: parseDomains(cert),
|
||||
notBefore: cert.notBefore,
|
||||
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://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/x509-1.1.js#L2404-L2412
|
||||
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/asn1x509-1.0.js#L3526-L3535
|
||||
* https://datatracker.ietf.org/doc/html/rfc5280
|
||||
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
|
||||
*
|
||||
* @private
|
||||
* @param {string} field CSR subject field
|
||||
* @returns {string} ASN.1 jsrsasign character string type
|
||||
* @param {string} field CSR subject field name
|
||||
* @returns {string} ASN.1 character string type
|
||||
*/
|
||||
|
||||
function getCsrAsn1CharStringType(field) {
|
||||
switch (field) {
|
||||
case 'C':
|
||||
return 'prn';
|
||||
return 'printableString';
|
||||
case 'E':
|
||||
return 'ia5';
|
||||
return 'ia5String';
|
||||
default:
|
||||
return 'utf8';
|
||||
return 'utf8String';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,6 +373,8 @@ function getCsrAsn1CharStringType(field) {
|
|||
/**
|
||||
* Create array of subject fields for a Certificate Signing Request
|
||||
*
|
||||
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
|
||||
*
|
||||
* @private
|
||||
* @param {object} input Key-value of subject fields
|
||||
* @returns {object[]} Certificate Signing Request subject array
|
||||
|
@ -399,7 +384,7 @@ function createCsrSubject(input) {
|
|||
return Object.entries(input).reduce((result, [type, value]) => {
|
||||
if (value) {
|
||||
const ds = getCsrAsn1CharStringType(type);
|
||||
result.push([{ type, value, ds }]);
|
||||
result.push({ [type]: [{ [ds]: value }] });
|
||||
}
|
||||
|
||||
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
|
||||
* @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) {
|
||||
return altNames.map((value) => {
|
||||
const key = net.isIP(value) ? 'ip' : 'dns';
|
||||
return { [key]: value };
|
||||
});
|
||||
function createSubjectAltNameExtension(altNames) {
|
||||
return new x509.SubjectAlternativeNameExtension(altNames.map((value) => {
|
||||
const type = net.isIP(value) ? 'ip' : 'dns';
|
||||
return { type, value };
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
@ -431,14 +416,14 @@ function formatCsrAltNames(altNames) {
|
|||
* @param {object} data
|
||||
* @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 {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.state] State or province
|
||||
* @param {string} [data.locality] City
|
||||
* @param {string} [data.organization] Organization name
|
||||
* @param {string} [data.organizationUnit] Organizational unit name
|
||||
* @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]
|
||||
*
|
||||
* @example Create a Certificate Signing Request
|
||||
|
@ -479,7 +464,7 @@ function formatCsrAltNames(altNames) {
|
|||
* }, certificateKey);
|
||||
*/
|
||||
|
||||
async function createCsr(data, keyPem = null) {
|
||||
exports.createCsr = async (data, keyPem = null) => {
|
||||
if (!keyPem) {
|
||||
keyPem = await createPrivateRsaKey(data.keySize);
|
||||
}
|
||||
|
@ -491,65 +476,52 @@ async function createCsr(data, keyPem = null) {
|
|||
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 */
|
||||
if (data.commonName && !data.altNames.includes(data.commonName)) {
|
||||
data.altNames.unshift(data.commonName);
|
||||
}
|
||||
|
||||
/* Subject */
|
||||
const subject = createCsrSubject({
|
||||
CN: data.commonName,
|
||||
C: data.country,
|
||||
ST: data.state,
|
||||
L: data.locality,
|
||||
O: data.organization,
|
||||
OU: data.organizationUnit,
|
||||
E: data.emailAddress
|
||||
});
|
||||
/* CryptoKeyPair and signing algorithm from private key */
|
||||
const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
|
||||
|
||||
/* SAN extension */
|
||||
if (data.altNames.length) {
|
||||
extensionRequests.push({
|
||||
extname: 'subjectAltName',
|
||||
array: formatCsrAltNames(data.altNames)
|
||||
});
|
||||
}
|
||||
const extensions = [
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
|
||||
|
||||
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
|
||||
createSubjectAltNameExtension(data.altNames)
|
||||
];
|
||||
|
||||
/* Create CSR */
|
||||
const csr = new jsrsasign.KJUR.asn1.csr.CertificationRequest({
|
||||
subject: { array: subject },
|
||||
sigalg: info.signatureAlgorithm,
|
||||
sbjprvkey: keyPem.toString(),
|
||||
sbjpubkey: jwk,
|
||||
extreq: extensionRequests
|
||||
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
|
||||
keys,
|
||||
extensions,
|
||||
signingAlgorithm,
|
||||
name: createCsrSubject({
|
||||
CN: data.commonName,
|
||||
C: data.country,
|
||||
ST: data.state,
|
||||
L: data.locality,
|
||||
O: data.organization,
|
||||
OU: data.organizationUnit,
|
||||
E: data.emailAddress
|
||||
})
|
||||
});
|
||||
|
||||
/* Done */
|
||||
const pem = csr.getPEM();
|
||||
const pem = csr.toString('pem');
|
||||
return [keyPem, Buffer.from(pem)];
|
||||
}
|
||||
|
||||
exports.createCsr = createCsr;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 {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]
|
||||
*
|
||||
* @example Create a ALPN certificate
|
||||
|
@ -564,45 +536,58 @@ exports.createCsr = createCsr;
|
|||
*/
|
||||
|
||||
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 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 */
|
||||
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 */
|
||||
const certificate = new jsrsasign.KJUR.asn1.x509.Certificate({
|
||||
subject,
|
||||
sbjpubkey,
|
||||
sigalg,
|
||||
version: 3,
|
||||
serial: { hex: Buffer.from(serial).toString('hex') },
|
||||
issuer: subject,
|
||||
notbefore: jsrsasign.datetozulu(now),
|
||||
notafter: jsrsasign.datetozulu(now),
|
||||
cakey: key.toString(),
|
||||
ext: extreq.concat([alpnExt])
|
||||
const cert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
keys,
|
||||
signingAlgorithm,
|
||||
extensions,
|
||||
serialNumber,
|
||||
notBefore: now,
|
||||
notAfter: now,
|
||||
name: createCsrSubject({
|
||||
CN: commonName
|
||||
})
|
||||
});
|
||||
|
||||
/* Done */
|
||||
const pem = certificate.getPEM();
|
||||
return [key, Buffer.from(pem)];
|
||||
const pem = cert.toString('pem');
|
||||
return [keyPem, Buffer.from(pem)];
|
||||
};
|
||||
|
||||
|
||||
|
@ -615,14 +600,20 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
|
|||
*/
|
||||
|
||||
exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
|
||||
const params = getCertificateParams(certPem);
|
||||
const expectedHex = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
|
||||
const acmeExt = (params.ext || []).find((e) => (e && e.extname && (e.extname === alpnAcmeIdentifierOID)));
|
||||
const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/* Decode extension value */
|
||||
const parsed = asn1js.fromBER(ext.value);
|
||||
const result = Buffer.from(parsed.result.valueBlock.valueHexView).toString('hex');
|
||||
|
||||
/* Return true if match */
|
||||
return (acmeExt.extn.octstr.hex === expectedHex);
|
||||
return (result === expected);
|
||||
};
|
||||
|
|
|
@ -64,7 +64,7 @@ class HttpClient {
|
|||
/**
|
||||
* 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}
|
||||
*/
|
||||
|
@ -104,7 +104,7 @@ class HttpClient {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@ -267,7 +267,7 @@ class HttpClient {
|
|||
/**
|
||||
* 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 {object} payload Request payload
|
||||
|
@ -299,7 +299,7 @@ class HttpClient {
|
|||
const data = this.createSignedBody(url, payload, { nonce, kid });
|
||||
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)) {
|
||||
nonce = resp.headers['replay-nonce'] || null;
|
||||
attempts += 1;
|
||||
|
|
|
@ -93,7 +93,7 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
|
|||
*
|
||||
* @param {string} header Link header contents
|
||||
* @param {string} rel Link relation, default: `alternate`
|
||||
* @returns {array} Array of URLs
|
||||
* @returns {string[]} Array of URLs
|
||||
*/
|
||||
|
||||
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 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
|
||||
* @returns {string} PEM encoded certificate chain
|
||||
*/
|
||||
|
|
|
@ -13,7 +13,7 @@ const { isAlpnCertificateAuthorizationValid } = require('./crypto');
|
|||
/**
|
||||
* 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} challenge Authorization challenge
|
||||
|
@ -85,7 +85,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
|
|||
/**
|
||||
* 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} challenge Authorization challenge
|
||||
|
@ -125,7 +125,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
|
|||
/**
|
||||
* 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} challenge Authorization challenge
|
||||
|
|
|
@ -10,10 +10,10 @@ const { crypto } = require('./../');
|
|||
|
||||
const emptyBodyChain1 = `
|
||||
-----BEGIN TEST-----
|
||||
a
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
-----BEGIN TEST-----
|
||||
b
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
|
||||
-----BEGIN TEST-----
|
||||
|
@ -22,7 +22,7 @@ b
|
|||
|
||||
|
||||
-----BEGIN TEST-----
|
||||
c
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
`;
|
||||
|
||||
|
@ -38,15 +38,15 @@ const emptyBodyChain2 = `
|
|||
-----END TEST-----
|
||||
|
||||
-----BEGIN TEST-----
|
||||
a
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
|
||||
|
||||
-----BEGIN TEST-----
|
||||
b
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
-----BEGIN TEST-----
|
||||
c
|
||||
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
|
||||
-----END TEST-----
|
||||
`;
|
||||
|
||||
|
@ -112,6 +112,11 @@ describe('crypto', () => {
|
|||
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`, () => {
|
||||
const jwk = crypto.getJwk(testPrivateKeys[n]);
|
||||
jwkSpecFn(jwk);
|
||||
|
@ -122,6 +127,11 @@ describe('crypto', () => {
|
|||
jwkSpecFn(jwk);
|
||||
});
|
||||
|
||||
it(`${n}/should get jwk from string`, () => {
|
||||
const jwk = crypto.getJwk(testPrivateKeys[n].toString());
|
||||
jwkSpecFn(jwk);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Certificate Signing Request
|
||||
|
@ -174,6 +184,15 @@ describe('crypto', () => {
|
|||
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 () => {
|
||||
await assert.isRejected(crypto.createCsr({
|
||||
commonName: testCsrDomain
|
||||
|
@ -217,6 +236,13 @@ describe('crypto', () => {
|
|||
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
|
||||
|
@ -232,6 +258,15 @@ describe('crypto', () => {
|
|||
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`, () => {
|
||||
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa'));
|
||||
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb'));
|
||||
|
@ -241,6 +276,10 @@ describe('crypto', () => {
|
|||
it(`${n}/should validate valid alpn certificate key authorization`, () => {
|
||||
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));
|
||||
});
|
||||
|
||||
it('should read certificate info from string', () => {
|
||||
[testCert, testSanCert].forEach((cert) => {
|
||||
const info = crypto.readCertificateInfo(cert.toString());
|
||||
spec.crypto.certificateInfo(info);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* 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', () => {
|
||||
[testPemKey, testCert, testSanCert].forEach((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', () => {
|
||||
const c1 = crypto.splitPemChain(emptyBodyChain1);
|
||||
const c2 = crypto.splitPemChain(emptyBodyChain2);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* Account
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.1.2
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.3
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.3.2
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
|
||||
*/
|
||||
|
||||
export interface Account {
|
||||
|
@ -31,8 +31,8 @@ export interface AccountUpdateRequest {
|
|||
/**
|
||||
* Order
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.1.3
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.4
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
|
||||
*/
|
||||
|
||||
export interface Order {
|
||||
|
@ -57,7 +57,7 @@ export interface OrderCreateRequest {
|
|||
/**
|
||||
* 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 {
|
||||
|
@ -77,9 +77,9 @@ export interface Identifier {
|
|||
/**
|
||||
* Challenge
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8555#section-8
|
||||
* https://tools.ietf.org/html/rfc8555#section-8.3
|
||||
* https://tools.ietf.org/html/rfc8555#section-8.4
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-8
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
|
||||
*/
|
||||
|
||||
export interface ChallengeAbstract {
|
||||
|
@ -106,7 +106,7 @@ export type Challenge = HttpChallenge | DnsChallenge;
|
|||
/**
|
||||
* Certificate
|
||||
*
|
||||
* https://tools.ietf.org/html/rfc8555#section-7.6
|
||||
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
|
||||
*/
|
||||
|
||||
export enum CertificateRevocationReason {
|
||||
|
|
Loading…
Reference in New Issue