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

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

View File

@ -3,6 +3,7 @@
## v5.3.0
* `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)

View File

@ -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`.

View File

@ -6,7 +6,7 @@ First off this release drops support for Node LTS v10, v12 and v14, and the reas
## New native crypto interface
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).

View File

@ -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": {

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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);
};

View File

@ -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;

View File

@ -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
*/

View File

@ -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

View File

@ -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);

View File

@ -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 {