diff --git a/.github/workflows/commit-queue.yml b/.github/workflows/commit-queue.yml index a1d691e9e19..c44ccc48d2f 100644 --- a/.github/workflows/commit-queue.yml +++ b/.github/workflows/commit-queue.yml @@ -28,6 +28,7 @@ jobs: if: github.repository == 'nodejs/node' runs-on: ubuntu-slim outputs: + aged_prs: ${{ steps.get_candidate_prs.outputs.aged_prs }} candidates: ${{ steps.get_candidate_prs.outputs.candidates }} steps: - name: Get Pull Request Candidates @@ -50,6 +51,7 @@ jobs: --search "-label:blocked") candidates=$(printf '%s %s\n' "$fast_track_prs" "$aged_prs" | jq -r -s 'reduce .[] as $pr ([]; if index($pr) then . else . + [$pr] end) | join(" ")') + echo "aged_prs=$aged_prs" >> "$GITHUB_OUTPUT" echo "candidates=$candidates" >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ github.token }} @@ -93,6 +95,7 @@ jobs: curl -fsSLo "$readme" "https://github.com/${GITHUB_REPOSITORY}/raw/${GITHUB_SHA}/README.md" numbers= + lacks_second_approval_prs= # shellcheck disable=SC2086 for pr in $CANDIDATES; do metadata="${RUNNER_TEMP}/metadata-${pr}.json" @@ -139,6 +142,14 @@ jobs: if [ "$metadata_status" -ge 20 ] && [ "$metadata_status" -le 29 ]; then echo "pr ${pr} skipped, not ready to land" echo "reason codes: ${metadata_reason_codes}" + if jq -e ' + (.reasonCodes | index("wait-time")) and + (.pullRequest.labels | index("lacks-second-approval") | not) + ' "$metadata" > /dev/null; then + case " $AGED_PRS " in + *" $pr "*) lacks_second_approval_prs="$lacks_second_approval_prs $pr" ;; + esac + fi continue fi @@ -148,11 +159,27 @@ jobs: done numbers=$(echo "$numbers" | xargs) + lacks_second_approval_prs=$(echo "$lacks_second_approval_prs" | xargs) echo "numbers=$numbers" >> "$GITHUB_OUTPUT" + echo "lacks_second_approval_prs=$lacks_second_approval_prs" >> "$GITHUB_OUTPUT" env: + AGED_PRS: ${{ needs.get_candidate_prs.outputs.aged_prs }} CANDIDATES: ${{ needs.get_candidate_prs.outputs.candidates }} GH_TOKEN: ${{ github.token }} + - name: Label Pull Requests Lacking a Second Approval + if: steps.get_mergeable_prs.outputs.lacks_second_approval_prs != '' + run: | + # shellcheck disable=SC2086 + for pr in $PULL_REQUESTS; do + if ! gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --add-label 'lacks-second-approval'; then + echo "::warning::Failed to add lacks-second-approval to PR ${pr}" + fi + done + env: + GH_TOKEN: ${{ secrets.GH_USER_TOKEN }} + PULL_REQUESTS: ${{ steps.get_mergeable_prs.outputs.lacks_second_approval_prs }} + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 if: steps.get_mergeable_prs.outputs.numbers != '' with: diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 4c61a34a003..b198122d135 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -423,14 +423,6 @@ console.log(challenge.toString('utf8')); // Prints: the challenge as a UTF8 string ``` -```cjs -const { Certificate } = require('node:crypto'); -const spkac = getSpkacSomehow(); -const challenge = Certificate.exportChallenge(spkac); -console.log(challenge.toString('utf8')); -// Prints: the challenge as a UTF8 string -``` - ### Static method: `Certificate.exportPublicKey(spkac[, encoding])` * Returns: {string\[]} An array of the names of the supported hash algorithms, such as `'RSA-SHA256'`. Hash algorithms are also called "digest" algorithms. +This is the authoritative Node.js list of hash algorithms available to +[`crypto.createHash()`][] and [`crypto.hash()`][] in the current process. With +OpenSSL 3 or later, the list depends on the loaded providers and the default +property query in effect when the list is first generated. Some listed +algorithms can require API-specific options, such as `outputLength` for XOF +hash functions. + +A listed hash algorithm is not necessarily supported by APIs that combine a +digest with another cryptographic operation, such as HMAC, key derivation, or +signing. Those operations can apply additional restrictions. + ```mjs const { getHashes, @@ -4995,6 +5107,11 @@ added: - v21.7.0 - v20.12.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/65484 + description: Hash algorithms exposed by OpenSSL providers are now + supported. The `functionName` and `customization` options + were added for cSHAKE hash functions. - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/64000 description: The `outputLength` option is now required for XOF @@ -5016,6 +5133,12 @@ changes: into a `TypedArray` using either `TextEncoder` or `Buffer.from()` and passing the encoded `TypedArray` into this API instead. * `options` {Object|string} + * `customization` {string|ArrayBuffer|Buffer|TypedArray|DataView} For cSHAKE + hash functions, specifies the customization byte string. **Default:** an + empty byte string. + * `functionName` {string|ArrayBuffer|Buffer|TypedArray|DataView} For cSHAKE + hash functions, specifies the NIST function-name byte string. **Default:** + an empty byte string. * `outputEncoding` {string} [Encoding][encoding] used to encode the returned digest. **Default:** `'hex'`. * `outputLength` {number} For XOF hash functions such as 'shake256', @@ -5028,10 +5151,21 @@ the object-based `crypto.createHash()` when hashing a smaller amount of data (<= 5MB) that's readily available. If the data can be big or if it is streamed, it's still recommended to use `crypto.createHash()` instead. -The `algorithm` is dependent on the available algorithms supported by the -version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. -On recent releases of OpenSSL, `openssl list -digest-algorithms` will -display the available digest algorithms. +The available algorithms depend on the version and configuration of OpenSSL on +the platform. Examples are `'sha256'` and `'sha512'`. Use +[`crypto.getHashes()`][] to obtain the list of hash algorithms available to the +Node.js process. + +The `functionName` and `customization` options apply only to cSHAKE-128 and +cSHAKE-256. They are supported only when Node.js is built with OpenSSL 4.0 or +later and the selected provider supports the corresponding digest parameters. +Strings are encoded as UTF-8, and neither strings nor byte values may contain +NUL bytes. Both options default to an empty byte string. For OpenSSL's built-in +providers, `functionName` is case-sensitive and must be `''`, `'TupleHash'`, +`'ParallelHash'`, or `'KMAC'`. Other providers can impose different +restrictions. With both options empty, cSHAKE produces the same output as the +corresponding SHAKE function for the same output length. `cshake-128` and +`cshake-256` default to output lengths of 32 and 64 bytes, respectively. If `options` is a string, then it specifies the `outputEncoding`. @@ -5103,6 +5237,10 @@ changes: HKDF is a simple key derivation function defined in RFC 5869. The given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. +The available digest algorithms depend on the version and configuration of +OpenSSL. HKDF uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or HKDF. The supplied `callback` function is called with two arguments: `err` and `derivedKey`. If an error occurs while deriving the key, `err` will be set; @@ -5162,6 +5300,10 @@ changes: Provides a synchronous HKDF key derivation function as defined in RFC 5869. The given `ikm`, `salt` and `info` are used with the `digest` to derive a key of `keylen` bytes. +The available digest algorithms depend on the version and configuration of +OpenSSL. HKDF uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or HKDF. The successfully generated `derivedKey` will be returned as an {ArrayBuffer}. @@ -5271,8 +5413,10 @@ pbkdf2('secret', 'salt', 100000, 64, 'sha512', (err, derivedKey) => { }); ``` -An array of supported digest functions can be retrieved using -[`crypto.getHashes()`][]. +The available digest algorithms depend on the version and configuration of +OpenSSL. PBKDF2 uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or PBKDF2. This API uses libuv's threadpool, which can have surprising and negative performance implications for some applications; see the @@ -5344,8 +5488,10 @@ const key = pbkdf2Sync('secret', 'salt', 100000, 64, 'sha512'); console.log(key.toString('hex')); // '3745e48...08d59ae' ``` -An array of supported digest functions can be retrieved using -[`crypto.getHashes()`][]. +The available digest algorithms depend on the version and configuration of +OpenSSL. PBKDF2 uses HMAC internally. [`crypto.getHashes()`][] lists algorithms +available to the hashing APIs, but not every listed algorithm is necessarily +suitable for HMAC or PBKDF2. ### `crypto.privateDecrypt(privateKey, buffer)` @@ -5404,6 +5550,10 @@ changes: Decrypts `buffer` with `privateKey`. `buffer` was previously encrypted using the corresponding public key, for example using [`crypto.publicEncrypt()`][]. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the active RSA implementation can impose additional restrictions on digests +used for OAEP or MGF1. + If `privateKey` is not a [`KeyObject`][], this function behaves as if `privateKey` had been passed to [`crypto.createPrivateKey()`][]. If it is an object, the `padding` property can be passed. Otherwise, this function uses @@ -5561,6 +5711,10 @@ Encrypts the content of `buffer` with `key` and returns a new [`Buffer`][] with encrypted content. The returned data can be decrypted using the corresponding private key, for example using [`crypto.privateDecrypt()`][]. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the active RSA implementation can impose additional restrictions on digests +used for OAEP or MGF1. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPublicKey()`][]. If it is an object, the `padding` property can be passed. Otherwise, this function uses @@ -6347,6 +6501,10 @@ dependent upon the key type. `algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and ML-DSA. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the key type and signature scheme determine whether a listed digest can be +used for signing. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPrivateKey()`][]. When `key` is a string, `ArrayBuffer`, [`Buffer`][], `TypedArray`, or `DataView`, it must contain PEM-encoded key @@ -6489,6 +6647,10 @@ key type. `algorithm` is required to be `null` or `undefined` for Ed25519, Ed448, and ML-DSA. +[`crypto.getHashes()`][] lists algorithms available to the hashing APIs, but +the key type and signature scheme determine whether a listed digest can be +used for verification. + If `key` is not a [`KeyObject`][], this function behaves as if `key` had been passed to [`crypto.createPublicKey()`][]. When `key` is a string, `ArrayBuffer`, [`Buffer`][], `TypedArray`, or `DataView`, it must contain PEM-encoded key @@ -6732,6 +6894,79 @@ try { console.log(receivedPlaintext); ``` +### CBC-CTS mode + +For CBC ciphertext stealing (CBC-CTS) ciphers, the `ctsMode` option to +[`crypto.createCipheriv()`][] or [`crypto.createDecipheriv()`][] selects the +variant: + +* `'CS1'` is the default. For block-aligned input, its output is the same as CBC + mode. +* `'CS2'` is also the same as CBC for block-aligned input. For input with a + partial final block, it swaps the final full and partial ciphertext blocks + relative to CS1. +* `'CS3'` is the Kerberos 5 variant. It uses the CS2 ordering for a partial + final block and swaps the final two ciphertext blocks even for block-aligned + input. + +Encryption and decryption must use the same variant. The option is available +only with CBC-CTS provider ciphers on OpenSSL 3.0 or later. + +Applications which use this mode must adhere to these restrictions: + +* The plaintext or ciphertext must be at least one block long. +* The ciphertext has the same length as the plaintext. +* `cipher.update()` or `decipher.update()` must be called exactly once with all + input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may + fail because CBC-CTS does not accept multiple data updates. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. +* `crypto.getCipherInfo()` reports the base mode as `'cbc'`. + +### XTS mode + +XTS ciphers operate on independently tweakable data units. A `Cipheriv` or +`Decipheriv` instance represents one complete data unit, and its `iv` argument +provides the 16-byte tweak. Use a new instance with the appropriate positional +tweak for each different logical data unit. + +Applications which use XTS mode must adhere to these restrictions: + +* The plaintext or ciphertext must be at least one 16-byte block. Its length + does not have to be a multiple of 16 bytes because XTS uses ciphertext + stealing for a final partial block. +* The ciphertext has the same length as the plaintext. +* `cipher.update()` or `decipher.update()` must be called exactly once with all + input data. Stream methods such as `write(data)`, `end(data)`, or `pipe()` may + fail because XTS does not accept multiple data updates. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. + +For `sm4-xts`, the `xtsStandard` option to [`crypto.createCipheriv()`][] or +[`crypto.createDecipheriv()`][] selects either the default `'GB'` variant from +GB/T 17964-2021 or the `'IEEE'` variant from IEEE Std 1619-2007. Encryption and +decryption must use the same variant. The option is available only for +`sm4-xts`; it does not apply to AES-XTS ciphers. OpenSSL's default provider +supports `sm4-xts` in OpenSSL 3.2 or later. + +### AES key wrap modes + +AES key wrap (`AES-WRAP`) and AES key wrap with padding (`AES-WRAP-PAD`) +ciphers operate on a complete key-data value rather than on an incremental byte +stream. The inverse-transform variants have the same processing restrictions. + +Applications which use an AES key wrap cipher must adhere to these +restrictions: + +* `cipher.update()` or `decipher.update()` must be called exactly once with the + complete, non-empty input value. +* Do not use AES key wrap ciphers as generic [`stream.Transform`][] streams. + Methods such as `write(data)`, `end(data)`, and `pipe()` can split one value + across multiple updates, with each update being treated as a separate wrap or + unwrap operation. +* `cipher.final()` or `decipher.final()` must still be called to complete the + operation. + ### SIV and GCM-SIV modes `SIV`[^openssl30] and `GCM-SIV`[^openssl32] are supported [AEAD algorithms][] @@ -7157,6 +7392,8 @@ See the [list of SSL OP Flags][] for details. [^openssl35]: Requires OpenSSL >= 3.5 [AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption +[AES key wrap modes]: #aes-key-wrap-modes +[CBC-CTS mode]: #cbc-cts-mode [CCM mode]: #ccm-mode [CVE-2021-44532]: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44532 [Caveats]: #support-for-weak-or-compromised-algorithms @@ -7186,7 +7423,9 @@ See the [list of SSL OP Flags][] for details. [RFC 7517]: https://www.rfc-editor.org/rfc/rfc7517.txt [RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt [RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt +[SIV and GCM-SIV modes]: #siv-and-gcm-siv-modes [Web Crypto API documentation]: webcrypto.md +[XTS mode]: #xts-mode [`--allow-openssl-store`]: cli.md#--allow-openssl-store [`--enable-fips`]: cli.md#--enable-fips [`--force-fips`]: cli.md#--force-fips @@ -7216,6 +7455,7 @@ See the [list of SSL OP Flags][] for details. [`crypto.createVerify()`]: #cryptocreateverifyalgorithm-options [`crypto.generateKey()`]: #cryptogeneratekeytype-options-callback [`crypto.generateKeyPair()`]: #cryptogeneratekeypairtype-options-callback +[`crypto.getCiphers()`]: #cryptogetciphers [`crypto.getCurves()`]: #cryptogetcurves [`crypto.getDiffieHellman()`]: #cryptogetdiffiehellmangroupname [`crypto.getFips()`]: #cryptogetfips @@ -7248,6 +7488,7 @@ See the [list of SSL OP Flags][] for details. [`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist [`sign.sign()`]: #signsignprivatekey-outputencoding [`sign.update()`]: #signupdatedata-inputencoding +[`stream.Transform`]: stream.md#class-streamtransform [`stream.Writable` options]: stream.md#new-streamwritableoptions [`stream.transform` options]: stream.md#new-streamtransformoptions [`util.promisify()`]: util.md#utilpromisifyoriginal diff --git a/doc/contributing/commit-queue.md b/doc/contributing/commit-queue.md index e9669bf3318..1cd35a32718 100644 --- a/doc/contributing/commit-queue.md +++ b/doc/contributing/commit-queue.md @@ -8,8 +8,11 @@ landing process by automating it via GitHub Actions. With it, collaborators can queue pull requests for landing by adding the `commit-queue` label to a PR. The selector checks readiness with `@node-core/utils`. If the pull request is only blocked on a deferrable condition, currently wait time, the queue leaves the -label in place and retries later. Other failures continue to the existing -landing and failure-reporting path. +label in place and retries later. For pull requests that are at least two days +old and still waiting for a second approval, the queue adds the +`lacks-second-approval` label. The queue removes that label when it removes the +`commit-queue` label. Other failures continue to the existing landing and +failure-reporting path. To make the Commit Queue squash all the commits of a pull request into the first one, add the `commit-queue-squash` label. diff --git a/tools/actions/commit-queue.sh b/tools/actions/commit-queue.sh index 9fb74ed9cad..e260773d14a 100755 --- a/tools/actions/commit-queue.sh +++ b/tools/actions/commit-queue.sh @@ -7,6 +7,7 @@ DEFAULT_BRANCH=main COMMIT_QUEUE_LABEL="commit-queue" COMMIT_QUEUE_FAILED_LABEL="commit-queue-failed" +LACKS_SECOND_APPROVAL_LABEL="lacks-second-approval" cqurl="${GITHUB_SERVER_URL:?}/${GITHUB_REPOSITORY:?}/actions/runs/${GITHUB_RUN_ID:?}" @@ -24,11 +25,43 @@ escape_code_block_or_line() { printf '%s%s%s%s%s\n' "$fence" "$sep" "$1" "$sep" "$fence" } +edit_pr_labels() { + pr=$1 + failure_mode=$2 + shift 2 + if gh -R "$GITHUB_REPOSITORY" pr edit "$pr" "$@"; then + return + fi + if [ "$failure_mode" = warn ]; then + echo "::warning::Failed to update labels for PR $pr" + return + fi + return 1 +} + +remove_labels_if_present() { + pr=$1 + shift + labels= + for label in "$@"; do + if jq -e --arg label "$label" \ + 'map(.name) | index($label)' < labels.json > /dev/null; then + labels="${labels}${labels:+,}${label}" + fi + done + + if [ -n "$labels" ]; then + edit_pr_labels "$pr" warn --remove-label "$labels" + fi +} + commit_queue_failed() { pr=$1 reported_failure=${2:-} - gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --add-label "${COMMIT_QUEUE_FAILED_LABEL}" --remove-label "${COMMIT_QUEUE_LABEL}" + edit_pr_labels "$pr" required --add-label "$COMMIT_QUEUE_FAILED_LABEL" \ + --remove-label "$COMMIT_QUEUE_LABEL" + remove_labels_if_present "$pr" "$LACKS_SECOND_APPROVAL_LABEL" last_output_line=$(awk 'NF { line = $0 } END { sub(/^[[:space:]]*/, "", line); print line }' output) # shellcheck disable=SC2016 @@ -145,8 +178,9 @@ for pr in "$@"; do [ -z "$MULTIPLE_COMMIT_POLICY" ] && gh -R "$GITHUB_REPOSITORY" pr close "$pr" - # Delete the commit queue label (but ignore errors, it's no big deal if a closed PR still has the label) - gh -R "$GITHUB_REPOSITORY" pr edit "$pr" --remove-label "$COMMIT_QUEUE_LABEL" || true + # Delete the commit queue labels (but ignore errors, it's no big deal if a closed PR still has them) + remove_labels_if_present "$pr" "$COMMIT_QUEUE_LABEL" \ + "$LACKS_SECOND_APPROVAL_LABEL" done rm -f labels.json