From 80cd1bfc8e9d526b2a204bcfcb443ad8d5b35ddd Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Sat, 3 Feb 2024 19:24:11 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20with?= =?UTF-8?q?=205=20commits=20[trident-sync]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update IETF links Fix misc typos Forgot SAN extension for self-signed ALPN certs Replace jsrsasign dep with @peculiar/x509 --- packages/core/acme-client/CHANGELOG.md | 17 +- packages/core/acme-client/README.md | 6 +- packages/core/acme-client/docs/upgrade-v5.md | 2 +- packages/core/acme-client/package.json | 3 +- packages/core/acme-client/src/api.js | 26 +- packages/core/acme-client/src/client.js | 34 +- packages/core/acme-client/src/crypto/forge.js | 4 +- packages/core/acme-client/src/crypto/index.js | 345 +++++++++--------- packages/core/acme-client/src/http.js | 8 +- packages/core/acme-client/src/util.js | 4 +- packages/core/acme-client/src/verify.js | 6 +- .../core/acme-client/test/20-crypto.spec.js | 79 +++- packages/core/acme-client/types/rfc8555.d.ts | 20 +- 13 files changed, 307 insertions(+), 247 deletions(-) diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md index 894dac23..a281ba52 100644 --- a/packages/core/acme-client/CHANGELOG.md +++ b/packages/core/acme-client/CHANGELOG.md @@ -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) diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md index 7d9eb09e..3dff3cfa 100644 --- a/packages/core/acme-client/README.md +++ b/packages/core/acme-client/README.md @@ -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`. diff --git a/packages/core/acme-client/docs/upgrade-v5.md b/packages/core/acme-client/docs/upgrade-v5.md index a89bb79f..f34fd6e6 100644 --- a/packages/core/acme-client/docs/upgrade-v5.md +++ b/packages/core/acme-client/docs/upgrade-v5.md @@ -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). diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json index 771bb55f..3cf35551 100644 --- a/packages/core/acme-client/package.json +++ b/packages/core/acme-client/package.json @@ -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": { diff --git a/packages/core/acme-client/src/api.js b/packages/core/acme-client/src/api.js index 84fe0f88..31c06f52 100644 --- a/packages/core/acme-client/src/api.js +++ b/packages/core/acme-client/src/api.js @@ -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} 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} 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} 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} 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} 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} 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} 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} HTTP response diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js index 2d3fa89c..fea97849 100644 --- a/packages/core/acme-client/src/client.js +++ b/packages/core/acme-client/src/client.js @@ -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} 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} 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} 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} 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} 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} 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} 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} 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} 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 diff --git a/packages/core/acme-client/src/crypto/forge.js b/packages/core/acme-client/src/crypto/forge.js index f22425de..5b66327a 100644 --- a/packages/core/acme-client/src/crypto/forge.js +++ b/packages/core/acme-client/src/crypto/forge.js @@ -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] diff --git a/packages/core/acme-client/src/crypto/index.js b/packages/core/acme-client/src/crypto/index.js index d7a13b7d..da0df321 100644 --- a/packages/core/acme-client/src/crypto/index.js +++ b/packages/core/acme-client/src/crypto/index.js @@ -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} [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} [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} [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); }; diff --git a/packages/core/acme-client/src/http.js b/packages/core/acme-client/src/http.js index 49f79322..b3580263 100644 --- a/packages/core/acme-client/src/http.js +++ b/packages/core/acme-client/src/http.js @@ -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} 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; diff --git a/packages/core/acme-client/src/util.js b/packages/core/acme-client/src/util.js index 73ecdd3b..c0c56370 100644 --- a/packages/core/acme-client/src/util.js +++ b/packages/core/acme-client/src/util.js @@ -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 */ diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js index 2ab95e8f..c0456497 100644 --- a/packages/core/acme-client/src/verify.js +++ b/packages/core/acme-client/src/verify.js @@ -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 diff --git a/packages/core/acme-client/test/20-crypto.spec.js b/packages/core/acme-client/test/20-crypto.spec.js index cbcd9f39..71e5277f 100644 --- a/packages/core/acme-client/test/20-crypto.spec.js +++ b/packages/core/acme-client/test/20-crypto.spec.js @@ -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); diff --git a/packages/core/acme-client/types/rfc8555.d.ts b/packages/core/acme-client/types/rfc8555.d.ts index 51b6f3d9..2f88ab75 100644 --- a/packages/core/acme-client/types/rfc8555.d.ts +++ b/packages/core/acme-client/types/rfc8555.d.ts @@ -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 {