From 2b8cd224234a3c40a576f947cdd744e4b59a3a84 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 21:16:27 -0700 Subject: [PATCH 1/4] Add WebCrypto HMAC: crypto.subtle importKey, sign, verify and digest crypto.subtle.importKey('raw', ...) for HMAC keys, HMAC sign and verify, and the SHA-1/SHA-2 digests compile through the crypto global, with CryptoKey values as native handles. scr_webcrypto.c is only the WebCrypto layer (Web IDL conversion, algorithm normalization, the CryptoKey handle, promise wrapping and Node's TypeErrors and DOMExceptions) over node:crypto's digest core and deferred promise queue. It links only into programs that call these methods (the new webcrypto link feature). Other algorithms, the jwk format and the other SubtleCrypto methods are SC2020 refusals. The real jose@6.2.12 HS256 path (SignJWT, jwtVerify) now runs natively. It also needed four compiler fixes: object patterns of positions and length over arrays, the Uint8Array base64/hex feature probes, a compile-time String.fromCharCode.apply, and structuredClone of a parameter whose default is {}. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + .../node-v24-compatibility-meta.json | 2 +- .../src/generated/node-v24-compatibility.json | 86 +- .../generated/node-v24-backlog.json | 232 +++-- .../generated/node-v24-internal.json | 156 +++- internal/compatibility/static-support.json | 6 + .../ambient/scriptc-node-fallback.d.ts | 117 ++- .../src/backend/executable-features.ts | 1 + packages/compiler/src/backend/library-pack.ts | 1 + .../compiler/src/backend/llvm/lib-dispatch.ts | 1 + .../compiler/src/backend/llvm/lib-shared.ts | 9 + .../src/backend/native-link-info-core.ts | 3 + .../compiler/src/backend/native/contracts.ts | 2 + .../compiler/src/backend/native/executable.ts | 1 + .../src/backend/runtime-pack-targets.test.ts | 1 + .../compiler/src/backend/runtime-pack.test.ts | 1 + packages/compiler/src/backend/warm-cache.ts | 1 + .../src/executable/executable-cache.test.ts | 1 + .../src/executable/executable-cache.ts | 2 + .../src/frontend/lowering/builtins/strings.ts | 108 ++- .../frontend/lowering/builtins/webcrypto.ts | 130 +++ .../src/frontend/lowering/lower-calls.ts | 13 +- .../src/frontend/lowering/lower-exprs.ts | 27 + .../src/frontend/lowering/lower-modules.ts | 4 +- .../src/frontend/lowering/lower-stmts.ts | 96 +- .../compiler/src/frontend/lowering/lowerer.ts | 43 +- .../src/frontend/lowering/surfaces.ts | 14 + packages/compiler/src/frontend/type-mapper.ts | 30 + packages/compiler/src/index.ts | 1 + .../compiler/src/ir/builtin-signatures.ts | 8 + packages/compiler/src/ir/runtime-features.ts | 5 + packages/compiler/src/library/ir-policy.ts | 1 + packages/compiler/src/native/cli.ts | 1 + packages/compiler/src/native/prepare.ts | 1 + .../compiler/src/native/sanitizer.test.ts | 1 + packages/compiler/surface-manifest.json | 14 + .../6291-webcrypto-hmac-sign-verify.ts.json | 6 + .../corpus/6292-webcrypto-hmac-errors.ts.json | 6 + .../corpus/6293-webcrypto-digest.ts.json | 6 + .../corpus/6294-webcrypto-js-values.mjs.json | 6 + .../corpus/6295-jose-hs256/main.mjs.json | 6 + .../6296-array-position-patterns.ts.json | 6 + .../6297-library-runtime-idioms.mjs.json | 6 + .../diagnostics/webcrypto-refusals.ts.json | 6 + .../node-types/webcrypto-hmac.ts.json | 6 + .../runtime-pack-matrix.mjs | 1 + .../runtime-pack-matrix.mjs | 1 + packages/runtime/src/scr_crypto.h | 19 +- packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_webcrypto.c | 829 ++++++++++++++++++ .../corpus/6291-webcrypto-hmac-sign-verify.ts | 94 ++ tests/corpus/6292-webcrypto-hmac-errors.ts | 83 ++ tests/corpus/6293-webcrypto-digest.ts | 35 + tests/corpus/6294-webcrypto-js-values.mjs | 52 ++ tests/corpus/6295-jose-hs256/main.mjs | 70 ++ .../node_modules/jose/LICENSE.md | 21 + .../jose/dist/types/jwt/sign.d.ts | 21 + .../jose/dist/types/jwt/verify.d.ts | 35 + .../node_modules/jose/dist/types/types.d.ts | 736 ++++++++++++++++ .../jose/dist/types/util/errors.d.ts | 182 ++++ .../node_modules/jose/dist/webapi/jwt/sign.js | 19 + .../jose/dist/webapi/jwt/verify.js | 13 + .../jose/dist/webapi/lib/buffer_utils.js | 75 ++ .../jose/dist/webapi/lib/jws_algorithms.js | 74 ++ .../jose/dist/webapi/lib/jws_sign.js | 46 + .../jose/dist/webapi/lib/jws_verify.js | 86 ++ .../jose/dist/webapi/lib/jwt_claims_set.js | 146 +++ .../node_modules/jose/dist/webapi/lib/key.js | 155 ++++ .../jose/dist/webapi/lib/key_descriptor.js | 9 + .../jose/dist/webapi/lib/validate.js | 122 +++ .../jose/dist/webapi/util/base64url.js | 16 + .../jose/dist/webapi/util/errors.js | 109 +++ .../node_modules/jose/package.json | 17 + tests/corpus/6296-array-position-patterns.ts | 23 + tests/corpus/6297-library-runtime-idioms.mjs | 34 + tests/diagnostics/destructuring-fences.ts | 4 +- tests/diagnostics/webcrypto-refusals.ts | 18 + tests/fixtures/node-types/fenced.ts | 7 + tests/fixtures/node-types/webcrypto-hmac.ts | 25 + .../__snapshots__/destructuring-fences.ts.txt | 6 +- .../__snapshots__/node-types-fenced.txt | 13 +- .../__snapshots__/webcrypto-refusals.ts.txt | 26 + tests/harness/errors.test.ts | 62 ++ tests/harness/project-config.test.ts | 18 + 84 files changed, 4290 insertions(+), 188 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/webcrypto.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json create mode 100644 packages/runtime/src/scr_webcrypto.c create mode 100644 tests/corpus/6291-webcrypto-hmac-sign-verify.ts create mode 100644 tests/corpus/6292-webcrypto-hmac-errors.ts create mode 100644 tests/corpus/6293-webcrypto-digest.ts create mode 100644 tests/corpus/6294-webcrypto-js-values.mjs create mode 100644 tests/corpus/6295-jose-hs256/main.mjs create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js create mode 100644 tests/corpus/6295-jose-hs256/node_modules/jose/package.json create mode 100644 tests/corpus/6296-array-position-patterns.ts create mode 100644 tests/corpus/6297-library-runtime-idioms.mjs create mode 100644 tests/diagnostics/webcrypto-refusals.ts create mode 100644 tests/fixtures/node-types/webcrypto-hmac.ts create mode 100644 tests/harness/__snapshots__/webcrypto-refusals.ts.txt diff --git a/.gitignore b/.gitignore index 4922ef6df..7b7bc57f2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ output/ !tests/fixtures/npm/cases/workspace-copied/node_modules/*/dist/ !tests/fixtures/provenance/node_modules/*/dist/ !tests/fixtures/npm-static/node_modules/*/dist/ +!tests/corpus/*/node_modules/*/dist/ .scriptc/ *.log .DS_Store diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 1cf76d3e4..1bf13ad1c 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "efc18bcd8a7a03fd8622", + "artifactVersion": "ef4244aca5b727e3683c", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 0e6387ea9..22a257728 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -329,9 +329,9 @@ "apiEntries": 92, "static": { "supported": 0, - "partial": 12, + "partial": 14, "refused": 1, - "not-implemented": 79, + "not-implemented": 77, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -854,9 +854,9 @@ "apiEntries": 156, "static": { "supported": 1, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 134, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -35693,9 +35693,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -36360,9 +36360,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80229,9 +80229,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80252,9 +80252,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80275,9 +80275,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80298,9 +80298,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80321,9 +80321,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80344,9 +80344,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80436,9 +80436,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80574,9 +80574,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80735,9 +80735,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80758,9 +80758,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -80804,9 +80804,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 7d0fcf75c..97dff333f 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,8 +7,8 @@ "tierItemCount": 3456, "tiers": { "static": { - "audit-partial": 1162, - "verify-gap": 2166, + "audit-partial": 1175, + "verify-gap": 2153, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -33213,12 +33213,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -33944,12 +33950,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81057,12 +81069,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81082,12 +81100,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81107,12 +81129,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81132,12 +81158,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81157,12 +81187,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81182,12 +81216,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81282,12 +81320,18 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] } } }, @@ -81432,12 +81476,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81607,12 +81658,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81632,12 +81690,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, @@ -81682,12 +81747,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:webcrypto", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c87f98fec..d04995caa 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -341,9 +341,9 @@ "apiEntries": 92, "static": { "supported": 0, - "partial": 12, + "partial": 14, "refused": 1, - "not-implemented": 79, + "not-implemented": 77, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -911,9 +911,9 @@ "apiEntries": 156, "static": { "supported": 1, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 145, + "not-implemented": 134, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52217,6 +52217,11 @@ "tests/corpus/5288-blob-validation.js", "tests/corpus/5290-blob-dynamic.js", "tests/corpus/5830-timers-promises-signal.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/text-codec-values/main.ts", "tests/harness/fetch-conformance.test.ts" ] @@ -52639,8 +52644,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -53313,8 +53325,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -106838,10 +106857,16 @@ "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/5442-crypto-random-values.ts", "tests/corpus/5443-crypto-random-arguments.js", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", + "tests/fixtures/node-types/webcrypto-hmac.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110373,8 +110398,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110396,8 +110428,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.CryptoKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110419,8 +110456,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110442,8 +110484,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.extractable", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110465,8 +110512,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.type", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110488,8 +110540,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.usages", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "ancestor" }, @@ -110580,8 +110637,15 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.SubtleCrypto", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs" + ] }, "anchorSource": "exact" }, @@ -110718,8 +110782,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.digest", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110879,8 +110951,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.importKey", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110902,8 +110982,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.sign", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110948,8 +111036,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.verify", + "tests": [ + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 584492bf1..b2371cb12 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -177,11 +177,17 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], + "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, {"chapter": "events", "symbols": ["Event"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5241-event-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5247-event-target-subclass-lifetimes.ts"]}, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 9655b7580..adc1816f7 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -511,14 +511,119 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* Capability-gated globals harness code probes behind feature guards - * (globalThis.crypto under a hasCrypto check, the SQLite-backed storage - * pair): typed `unknown` — no lowering exists, the guard is expected to - * be false in a compiled binary, and any reached use fences per site. */ -declare var crypto: unknown; +/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for + * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as + * exactly that surface (the declarations mirror @types/node's webcrypto + * shapes). Every other SubtleCrypto method and crypto member is absent, + * so a use is an honest type error. CryptoKey values are native handles + * whose four members read through the handle. */ +declare var crypto: Crypto; +/* The Crypto and SubtleCrypto constructors (feature probes only) stay + * `unknown`; the values keep this declaration order. */ declare var Crypto: unknown; -declare var CryptoKey: unknown; +declare var CryptoKey: { + prototype: CryptoKey; + new (): CryptoKey; +}; declare var SubtleCrypto: unknown; +type KeyUsage = + | "encrypt" + | "decrypt" + | "sign" + | "verify" + | "deriveKey" + | "deriveBits" + | "encapsulateBits" + | "decapsulateBits" + | "encapsulateKey" + | "decapsulateKey" + | "wrapKey" + | "unwrapKey"; +type KeyType = "private" | "public" | "secret"; +interface Algorithm { + name: string; +} +type AlgorithmIdentifier = Algorithm | string; +type HashAlgorithmIdentifier = AlgorithmIdentifier; +interface HmacImportParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface KeyAlgorithm { + name: string; +} +interface HmacKeyAlgorithm extends KeyAlgorithm { + hash: KeyAlgorithm; + length: number; +} +interface CryptoKey { + readonly algorithm: KeyAlgorithm; + readonly extractable: boolean; + readonly type: KeyType; + readonly usages: KeyUsage[]; +} +type KeyFormat = + | "jwk" + | "pkcs8" + | "raw" + | "raw-private" + | "raw-public" + | "raw-secret" + | "raw-seed" + | "spki"; +interface JsonWebKey { + alg?: string; + crv?: string; + d?: string; + dp?: string; + dq?: string; + e?: string; + ext?: boolean; + k?: string; + key_ops?: string[]; + kty?: string; + n?: string; + p?: string; + q?: string; + qi?: string; + use?: string; + x?: string; + y?: string; +} +/* The 'jwk' form type-checks like @types/node's; the compiler refuses it + * (the JsonWebKey import is not implemented). */ +interface SubtleCrypto { + digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + importKey( + format: "jwk", + keyData: JsonWebKey, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + importKey( + format: Exclude, + keyData: BufferSource, + algorithm: AlgorithmIdentifier | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; + sign(algorithm: AlgorithmIdentifier, key: CryptoKey, data: BufferSource): Promise; + verify( + algorithm: AlgorithmIdentifier, + key: CryptoKey, + signature: BufferSource, + data: BufferSource, + ): Promise; +} +interface Crypto { + readonly subtle: SubtleCrypto; +} + +/* Capability-gated globals harness code probes behind feature guards + * (the SQLite-backed storage pair): typed `unknown` — no lowering exists, + * the guard is expected to be false in a compiled binary, and any reached + * use fences per site. */ declare var localStorage: unknown; declare var sessionStorage: unknown; diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts index 553643470..11aad2e57 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -38,5 +38,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures { tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, }; } diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts index 833ef93e7..e1fd9784b 100644 --- a/packages/compiler/src/backend/library-pack.ts +++ b/packages/compiler/src/backend/library-pack.ts @@ -52,6 +52,7 @@ export async function compilePackedLibrary( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }; const selection = await loadRuntimePack({ diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts index f21ded106..d7d39f746 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -826,6 +826,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue { case "util": case "bigint": case "crypto": + case "webcrypto": case "buffer": case "bytes": case "tlsca": diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 87ffea5df..d62c4ca47 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -576,6 +576,11 @@ export const LIB_FN_SYMS: Record = { "crypto.generateKeySync": "scr_crypto_generate_key_sync", "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", + "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.sign": "scr_webcrypto_sign", + "webcrypto.verify": "scr_webcrypto_verify", + "webcrypto.digest": "scr_webcrypto_digest", + "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -1453,6 +1458,10 @@ export const USES_TIMERS_LIB_FNS = new Set([ "process.emitWarning", // Decipheriv.setAuthTag reports DEP0182 on the next tick. "crypto.cipherNew", + // WebCrypto results settle through the deferred crypto queue. + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", "process.emitWarningText", // The legacy url entries queue DEP0169/DEP0170 on the next tick. "url.legacyParse", diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index 8f69cea57..f11ad6992 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -48,6 +48,9 @@ export interface NativeLinkFeatures { * archive. */ cipher: boolean; tlsCa: boolean; + /** crypto.subtle importKey/sign/verify/digest and CryptoKey values: + * scr_webcrypto.c. */ + webcrypto: boolean; } export interface NativeLinkInfo { diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 5e981fe44..e0ac98afe 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -197,6 +197,8 @@ export interface CcOptions { /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links * the mbedTLS archive (shared with tls). */ cipher?: boolean; + /** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */ + webcrypto?: boolean; /** Internal compiler hook: called only after a strict native artifact hit * or a successful stable build has installed `outPath`. The executable * frontend cache uses it to publish its stamp after native dependencies diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 549e9ebbf..c495b6123 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -577,6 +577,7 @@ export async function compileCInternal( ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []), ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []), ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []), + ...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. ...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []), diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index faa335e33..fd57cb49d 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -119,6 +119,7 @@ const features = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test.each([ diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index ffe581366..61d1dba5e 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -56,6 +56,7 @@ const BASE: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts index 67e627c91..2cd111060 100644 --- a/packages/compiler/src/backend/warm-cache.ts +++ b/packages/compiler/src/backend/warm-cache.ts @@ -61,6 +61,7 @@ export async function warmNativeCaches( nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, }); diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts index e5887e855..1b42c3eca 100644 --- a/packages/compiler/src/executable/executable-cache.test.ts +++ b/packages/compiler/src/executable/executable-cache.test.ts @@ -57,6 +57,7 @@ const native: EarlyExecutableNativeFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; async function fixture(): Promise<{ diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts index fbd7e4edd..66ff502fe 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -79,6 +79,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; tlsCa: boolean; + webcrypto: boolean; } interface EarlyExecutableCacheStamp { @@ -190,6 +191,7 @@ const BOOLEAN_NATIVE_KEYS = [ "tls", "tlsCa", "cipher", + "webcrypto", ] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[]; function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures { diff --git a/packages/compiler/src/frontend/lowering/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index b117c24ff..614e8c873 100644 --- a/packages/compiler/src/frontend/lowering/builtins/strings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/strings.ts @@ -4,7 +4,19 @@ import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js"; import { lowerToNumberArgument } from "../lower-exprs.js"; -import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js"; +import { + BOOL, + DYN, + F64, + type IrExpr, + type IrStmt, + NULL_T, + STRING, + arrayOf, + bytesOf, + isUnitType, + typeEquals, +} from "../../../ir/ir.js"; /** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument * lowers as a number and packs into ONE f64[] array-literal argument @@ -17,6 +29,8 @@ export function lowerStringStaticCall( access: ts.PropertyAccessExpression, ): IrExpr | null { if (call.questionDotToken) return null; + const applied = lowerFromCharCodeApply(lowerer, call, access); + if (applied) return applied; const member = lowerer.stdlibGlobalMember(access, "String"); if (member !== "fromCharCode" && member !== "fromCodePoint" && member !== "raw") return null; const loc = locOf(call); @@ -105,6 +119,98 @@ export function lowerStringStaticCall( return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc }; } +/** `String.fromCharCode.apply(thisArg, codes)`, the btoa chunking idiom + * (`String.fromCharCode.apply(null, bytes.subarray(i, j))`): the receiver + * is ignored, and a typed array or number array forwards to the packed + * fromCharCode like a spread. An untyped list reads a Uint8Array the same + * way at run time; any other value keeps Function.prototype.apply's + * generic array-like call. Null for every other call. */ +function lowerFromCharCodeApply( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.name.text !== "apply" || access.questionDotToken) return null; + const target = access.expression; + if ( + !ts.isPropertyAccessExpression(target) || + lowerer.stdlibGlobalMember(target, "String") !== "fromCharCode" + ) { + return null; + } + if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) return null; + const listNode = call.arguments[1]!; + const checkerT = lowerer.typeOf(listNode); + const untyped = (checkerT.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; + const listT = untyped ? DYN : lowerer.mapTypeOf(checkerT); + if ( + listT === null || + (listT.kind !== "bytes" && listT.kind !== "dyn" && !typeEquals(listT, arrayOf(F64))) + ) { + return null; + } + const loc = locOf(call); + const thisArg = lowerer.lowerExprExpecting(call.arguments[0]!, DYN); + const stmts: IrStmt[] = [{ kind: "exprStmt", expr: thisArg, loc: thisArg.loc }]; + if (listT.kind !== "dyn") { + const list = lowerer.lowerExprExpecting(listNode, listT); + const result: IrExpr = { + kind: "libCall", + fn: "string.fromCharCode", + args: [list], + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; + } + const listLocal = lowerer.declareHiddenLocal("%charCodes", DYN); + stmts.push({ + kind: "varDecl", + localId: listLocal.id, + init: lowerer.lowerExprExpecting(listNode, DYN), + loc, + }); + const list = (): IrExpr => ({ kind: "varRef", localId: listLocal.id, type: DYN, loc }); + const bytes = bytesOf("u8"); + const generic: IrExpr = { + kind: "dynCheck", + value: { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(target, DYN), + method: "apply", + calleeName: "String.fromCharCode.apply", + args: [ + { + kind: "dynFrom", + value: { kind: "unitLit", unit: "null", type: NULL_T, loc }, + type: DYN, + loc, + }, + list(), + ], + type: DYN, + loc, + }, + type: STRING, + loc, + }; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "dynTest", test: "bytes", bytesElem: "u8", value: list(), type: BOOL, loc }, + then: { + kind: "libCall", + fn: "string.fromCharCode", + args: [{ kind: "dynCheck", value: list(), type: bytes, loc }], + type: STRING, + loc, + }, + else_: generic, + type: STRING, + loc, + }; + return { kind: "seqExpr", stmts, result, type: STRING, loc }; +} + /** `s.lastIndexOf(searchValue?, position?)` on string receivers, using UTF-16 * indices. An omitted search value searches for "undefined". Omitted or undefined positions clamp to the string's end; * MAX_SAFE_INTEGER has the same effect for every representable string. diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts new file mode 100644 index 000000000..8ff9d0a21 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -0,0 +1,130 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; +import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; +import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; + +/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, + * `sign`, `verify` and `digest` called on the crypto global (also through + * globalThis) lower to runtime entry points that take the argument vector + * as one dyn array and answer a promise, so Web IDL conversion, algorithm + * normalization and every error stay Node-shaped at run time. CryptoKey + * values are native handles; their type maps to dyn and their members + * are handle reads. Other SubtleCrypto methods are refused by name. */ + +const SUBTLE_LOWERED: ReadonlyMap = new Map([ + ["importKey", "webcrypto.importKey"], + ["sign", "webcrypto.sign"], + ["verify", "webcrypto.verify"], + ["digest", "webcrypto.digest"], +]); + +const SUBTLE_HINT = + "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + +/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + return ( + ts.isPropertyAccessExpression(node) && + !node.questionDotToken && + node.name.text === "subtle" && + stdlibGlobalNameOf(lowerer, node.expression) === "crypto" + ); +} + +/** `crypto.subtle.(...args)`: the lowered methods become their + * libCall over the argument vector; every other method is refused. */ +export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const callee = call.expression; + if (!ts.isPropertyAccessExpression(callee) || !isSubtleReceiver(lowerer, callee.expression)) { + return null; + } + const member = callee.name.text; + const fn = SUBTLE_LOWERED.get(member); + if (fn === undefined) lowerer.noLowering(`crypto.subtle.${member}`, callee, SUBTLE_HINT); + if (callee.questionDotToken || call.questionDotToken) { + lowerer.noLowering(`optional calls of crypto.subtle.${member}`, call, SUBTLE_HINT); + } + if (call.arguments.some(ts.isSpreadElement)) { + lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); + } + // JsonWebKey imports are not implemented: a literal 'jwk' format is + // refused here (the runtime refuses a computed one). + const format = call.arguments[0]; + if ( + member === "importKey" && + format !== undefined && + ts.isStringLiteralLike(format) && + format.text === "jwk" + ) { + lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); + } + const loc = locOf(call); + const args = call.arguments.map((arg) => + ts.isObjectLiteralExpression(arg) + ? lowerDynObjectLiteral(lowerer, arg) + : lowerer.lowerExprExpecting(arg, DYN), + ); + return { + kind: "libCall", + fn, + args: [{ kind: "dynArrLit", elems: args, type: DYN, loc }], + type: { kind: "promise", inner: member === "verify" ? BOOL : DYN }, + loc, + }; +} + +/** `v instanceof CryptoKey` against the global. */ +export function lowerCryptoKeyInstanceof( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + if (stdlibGlobalNameOf(lowerer, expr.right) !== "CryptoKey") return null; + const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); + return { + kind: "libCall", + fn: "webcrypto.isCryptoKey", + args: [value], + type: BOOL, + loc: locOf(expr), + }; +} + +/** TypeScript reads of `crypto.subtle` and its members outside a direct + * call: the global's dynamic object does not carry the lowered methods, + * so a detached read is refused rather than answering a value without + * them. JavaScript keeps the dynamic object's reads. */ +export function fenceWebCryptoMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression | ts.ElementAccessExpression, + use: "read" | "call", +): IrExpr | null { + if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null; + if (isJsSourceFile(access.getSourceFile())) return null; + if (isSubtleReceiver(lowerer, access)) { + lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT); + } + if (isSubtleReceiver(lowerer, access.expression)) { + lowerer.noLowering(`crypto.subtle.${access.name.text} as a value`, access, SUBTLE_HINT); + } + return null; +} + +/** Whether a checker type is one of the WebCrypto interfaces reached + * through the crypto global's members (SubtleCrypto, CryptoKey and the + * algorithm dictionaries). While `crypto` was typed `unknown` those member + * reads were untyped, so JavaScript destructurings of them keep the + * module-global path untyped bindings take. `Crypto` itself is excluded: + * the global was `unknown`, which already mapped to dyn. */ +export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean { + const symbol = type.getAliasSymbol() ?? type.getSymbol(); + return ( + symbol !== undefined && + symbol.name !== "Crypto" && + WEBCRYPTO_DYN_TYPES.has(symbol.name) && + lowerer.isStdlibSymbol(symbol) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 303bdf517..b027cd8d1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -671,12 +671,23 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { }; // The declared result is the value's own type (the generic's T): // validate the dyn copy back into it when the type can be checked; - // dyn-typed and unmappable results stay dyn values (JS files). + // dyn-typed and unmappable results stay dyn values (JS files). An + // empty record (`{}` inferred from a JavaScript `payload = {}` + // default) is the checker's supertype of whatever arrives, not a + // layout: checking into it would drop every member of the copy. const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr)); + const resultShape = + resultT?.kind === "record" ? lowerer.shapes.get(resultT.shapeId) : undefined; + const emptyRecord = + resultShape !== undefined && + !resultShape.tuple && + resultShape.indexValue === undefined && + resultShape.fields.length === 0; if ( resultT !== null && resultT.kind !== "dyn" && resultT.kind !== "void" && + !emptyRecord && canDynCheckTo( resultT, (id) => lowerer.shapes.get(id), diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 1b2bf00e5..7e3a70473 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -13,6 +13,7 @@ import { lowerReadlineProperty } from "./builtins/readline.js"; import { lowerBlobInstanceof } from "./builtins/blob.js"; import { lowerWebAssemblyTypeof, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerKeyObjectInstanceof } from "./builtins/key-object.js"; +import { lowerCryptoKeyInstanceof } from "./builtins/webcrypto.js"; import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js"; import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js"; import { STATS_TYPE_TESTS } from "./builtins/filesystem.js"; @@ -349,6 +350,26 @@ export function abstractPropertyDeclOf( * Narrowing a ParseArgsToken exposes one anonymous union arm, so its alias * identity no longer reaches mapType; declaration ancestry is the stable * provenance check. */ +/** The Uint8Array base64/hex prototype methods (ES2026) Node 24 lacks. */ +const UINT8ARRAY_BASE64_METHODS: ReadonlySet = new Set([ + "toBase64", + "toHex", + "setFromBase64", + "setFromHex", +]); + +/** `Uint8Array.prototype.` read off the global. */ +function isUint8ArrayBase64Probe(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { + if (!UINT8ARRAY_BASE64_METHODS.has(expr.name.text)) return false; + const proto = expr.expression; + return ( + ts.isPropertyAccessExpression(proto) && + !proto.questionDotToken && + proto.name.text === "prototype" && + stdlibGlobalNameOf(lowerer, proto.expression) === "Uint8Array" + ); +} + function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean { const sym = lowerer.checker.getSymbolAtLocation(expr.name); if (!sym) return false; @@ -2078,6 +2099,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ) { return dynUndefinedExpr(loc); } + // `Uint8Array.prototype.toBase64` and its siblings, the base64/hex + // feature probes libraries such as jose make: the methods are newer + // than Node 24, whose Uint8Array prototype answers undefined for them. + if (isUint8ArrayBase64Probe(lowerer, expr)) return dynUndefinedExpr(loc); if (expr.questionDotToken && expr.name.text === "electron") { const processProperty = lowerer.lowerProcessProperty(expr); if (processProperty) return processProperty; @@ -12637,6 +12662,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc if (blob) return blob; const keyObject = lowerKeyObjectInstanceof(lowerer, expr); if (keyObject) return keyObject; + const cryptoKey = lowerCryptoKeyInstanceof(lowerer, expr); + if (cryptoKey) return cryptoKey; const netAddress = lowerNetAddressInstanceof(lowerer, expr); if (netAddress) return netAddress; } diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index b375668bc..74924e775 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -92,6 +92,7 @@ import { import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js"; import { registerBuiltinClassAlias } from "./builtins/class-values.js"; import { stripTypeCasts } from "./builtins/arguments.js"; +import { isWebCryptoMemberType } from "./builtins/webcrypto.js"; import { bindingContextualGenericFnNodeOf, bindingGenericFnAliasInfoOf, @@ -1912,7 +1913,8 @@ export function collectGlobals( decl.initializer && !decl.type && !hasJsTypeAnnotation(decl) && - !lowerer.mapTypeOf(lowerer.typeOf(nameNode)) + (!lowerer.mapTypeOf(lowerer.typeOf(nameNode)) || + isWebCryptoMemberType(lowerer, lowerer.typeOf(nameNode))) ) { const symbol = lowerer.checker.getSymbolAtLocation(nameNode); if (symbol && !lowerer.globalsBySymbol.has(symbol)) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index dbfa30f63..7a4214ede 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2859,6 +2859,10 @@ export function lowerBindingPattern( lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); return; } + if (srcType.kind === "array" && isArrayPositionPattern(lowerer, pattern)) { + lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out); + return; + } if (srcType.kind !== "record") { lowerer.unsupported( "SC1031", @@ -3189,19 +3193,8 @@ function lowerMatchResultObjectPattern( ); } let value: IrExpr; - if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) { - const index = Number(propName); - const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; - value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { - kind: "arrayGet", - arr: srcRef(), - index: position, - type: srcType.elem, - loc, - }; - if (el.initializer) { - value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out); - } + if (ARRAY_POSITION_KEY.test(propName)) { + value = arrayPositionBinding(lowerer, el, srcRef, srcType, Number(propName), out); } else if (propName === "index" || propName === "input") { if (el.initializer) { lowerer.unsupported( @@ -3247,6 +3240,83 @@ function lowerMatchResultObjectPattern( } } +/** A canonical array index key (the numeric positions an object pattern + * reads off an array). */ +const ARRAY_POSITION_KEY = /^(?:0|[1-9]\d{0,8})$/; + +/** The positional read `{ : x }` takes off an array: the element, + * undefined past the end, with the binding default applied. */ +function arrayPositionBinding( + lowerer: Lowerer, + el: ts.BindingElement, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + index: number, + out: IrStmt[], +): IrExpr { + const loc = locOf(el); + const position: IrExpr = { kind: "numLit", value: index, type: F64, loc }; + const value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? { + kind: "arrayGet", + arr: srcRef(), + index: position, + type: srcType.elem, + loc, + }; + return el.initializer + ? arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out) + : value; +} + +/** Whether every element of an object pattern over an array names a + * numeric position or `length` (`const { 0: a, 1: b, length } = + * s.split(".")`), with no rest element. */ +function isArrayPositionPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean { + return pattern.elements.every((el) => { + if (el.name === undefined) return true; + if (el.dotDotDotToken) return false; + const prop = el.propertyName ?? el.name; + if ( + !ts.isIdentifier(prop) && + !ts.isComputedPropertyName(prop) && + !ts.isStringLiteralLike(prop) && + !ts.isNumericLiteral(prop) + ) + return false; + const name = patternKeyNameOf(lowerer, prop as ts.PropertyName); + return name !== null && (name === "length" || ARRAY_POSITION_KEY.test(name)); + }); +} + +/** An object pattern of positions and `length` over an array: each + * position is the read the equivalent array pattern makes, and `length` + * the array's length (always present, so its default never runs). */ +function lowerArrayPositionObjectPattern( + lowerer: Lowerer, + pattern: ts.ObjectBindingPattern, + srcRef: () => IrExpr, + srcType: IrType & { kind: "array" }, + isLet: boolean, + out: IrStmt[], +): void { + for (const el of pattern.elements) { + if (el.name === undefined) continue; + const name = patternKeyNameOf(lowerer, (el.propertyName ?? el.name) as ts.PropertyName)!; + const value: IrExpr = + name === "length" + ? { + kind: "arrIntrinsic", + method: "length", + receiver: srcRef(), + args: [], + type: F64, + loc: locOf(el), + } + : arrayPositionBinding(lowerer, el, srcRef, srcType, Number(name), out); + lowerer.bindPatternTarget(el.name, value, isLet, out); + } +} + /** Decode one non-rest object-assignment property into its static source * field, destination expression, and optional default initializer. */ function destructuringPropertyOf( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 4e130afd3..b2d302b55 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -30,6 +30,7 @@ import { refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js"; import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; +import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; import { hashStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; @@ -4872,13 +4873,42 @@ export class Lowerer { return out; }; const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => { - if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init); + if (ts.isObjectBindingPattern(name)) { + // `const { 0: a, 1: b, length } = parts` reads positions like the + // equivalent array pattern: each may sit past the end. + // Regex match results keep their own pattern lowering. + const arrayType = this.mapTypeOf(this.typeOf(init)); + const initType = this.checker.getNonNullableType(this.typeOf(init)); + const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol(); + const matchResult = + initSymbol !== undefined && + (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray"); + if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init); + const positions = name.elements.filter((el) => { + const key = el.propertyName; + return ( + key !== undefined && ts.isNumericLiteral(key) && /^(?:0|[1-9]\d{0,8})$/.test(key.text) + ); + }); + return scanPositions(positions, init, arrayType); + } if (!ts.isArrayBindingPattern(name)) return false; const sourceType = this.mapTypeOf(this.typeOf(init)); if (sourceType?.kind !== "array") return false; + return scanPositions( + name.elements.filter((el): el is ts.BindingElement => !ts.isOmittedExpression(el)), + init, + sourceType, + ); + }; + const scanPositions = ( + elements: readonly ts.BindingElement[], + init: ts.Expression, + sourceType: IrType & { kind: "array" }, + ): boolean => { let changed = false; - name.elements.forEach((el) => { - if (ts.isOmittedExpression(el) || el.name === undefined || el.dotDotDotToken) return; + elements.forEach((el) => { + if (el.name === undefined || el.dotDotDotToken) return; if (ts.isIdentifier(el.name)) { const symbol = symbolOf(el.name); if (symbol && !el.initializer && !optionalSymbols.has(symbol)) { @@ -7729,7 +7759,11 @@ export class Lowerer { access: ts.PropertyAccessExpression | ts.ElementAccessExpression, use: "read" | "call", ): IrExpr | null { - return fenceStaticResponseMember(this, access, use) ?? fenceStaticBlobMember(this, access, use); + return ( + fenceStaticResponseMember(this, access, use) ?? + fenceStaticBlobMember(this, access, use) ?? + fenceWebCryptoMember(this, access, use) + ); } fenceUnsupportedFetchConstructorMember( @@ -11523,6 +11557,7 @@ export class Lowerer { lowerStaticReadableStreamReaderCall(this, expr) ?? lowerStaticResponseCall(this, expr) ?? lowerStaticBlobCall(this, expr) ?? + lowerWebCryptoCall(this, expr) ?? lowerKeyObjectCall(this, expr) ?? lowerNetAddressCall(this, expr) ?? lowerFetchElementMethodCall(this, expr) ?? diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 8e02032ff..3f773c977 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2159,6 +2159,20 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ fns: ["blob.fileNew"], note: "a Blob with name and lastModified (Date.now() when absent)", }, + { + id: "stdlib.webcrypto.subtle", + kind: "stdlib", + name: "globalThis.crypto.subtle", + fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], + note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + }, + { + id: "stdlib.webcrypto.CryptoKey", + kind: "stdlib", + name: "CryptoKey", + fns: ["webcrypto.isCryptoKey"], + note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + }, { id: "stdlib.atomics", kind: "stdlib", diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 1aa5b1d1b..0562e673b 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -125,6 +125,19 @@ export function jsOpenObjectType( : type; } +/** The WebCrypto interfaces whose values are checked-dynamic: the crypto + * global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries + * (scr_webcrypto.c). */ +export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ + "Crypto", + "SubtleCrypto", + "CryptoKey", + "Algorithm", + "KeyAlgorithm", + "HmacImportParams", + "HmacKeyAlgorithm", +]); + export const FETCH_AMBIENT_TYPES = [ "Response", "Request", @@ -2437,6 +2450,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { ) { return DYN; } + // WebCrypto: the crypto global and its subtle object are dynamic + // objects, CryptoKey values are native handles (scr_webcrypto.c), and + // the algorithm dictionaries the methods take and keys report are plain + // checked-dynamic objects. + if ( + psym && + WEBCRYPTO_DYN_TYPES.has(psym.name) && + checker + .declarationsOf(psym) + .some( + (d) => + (ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) && + ctx.isStdlibFile(d.getSourceFile()), + ) + ) { + return DYN; + } // node:crypto's KeyObject: secret keys are native handles (scr_lib.c) // whose getters and methods dispatch through the handle. The JsonWebKey // that export() returns is a plain checked-dynamic object. diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 56b787e18..c616fec93 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -552,6 +552,7 @@ async function compileExecutableNative( tls: features.tls, tlsCa: features.tlsCa, cipher: features.cipher, + webcrypto: features.webcrypto, ...(onArtifactReady === undefined ? {} : { onArtifactReady }), ...(ffi === null ? {} diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index d2c503214..17259bd43 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2605,6 +2605,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "crypto.generateKeyCb": { argTypes: [DYN, DYN, null], result: VOID }, "crypto.keyObjectInput": { argTypes: [DYN], result: BYTES_U8 }, "crypto.isKeyObject": { argTypes: [DYN], result: BOOL }, + /** WebCrypto (scr_webcrypto.c): each method takes its argument vector as + * one dyn array and answers a promise that argument and validation + * failures reject. CryptoKey values are native handles. */ + "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, + "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 7df85c738..83596b7ea 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -43,6 +43,8 @@ export interface RuntimeFeatures { tls: boolean; tlsCa: boolean; cipher: boolean; + /** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */ + webcrypto: boolean; } const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ @@ -217,6 +219,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; const keepGoing = (): boolean => stopAt === undefined || !features[stopAt]; let tables: TypeTables | undefined; @@ -432,6 +435,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.dynAsync = true; features.inspect = true; } + // crypto.subtle and CryptoKey: scr_webcrypto.c. + if (fn.startsWith("webcrypto.")) features.webcrypto = true; } else { switch (node.kind) { case "regexLit": diff --git a/packages/compiler/src/library/ir-policy.ts b/packages/compiler/src/library/ir-policy.ts index 3c3855e5c..2d1bc85d4 100644 --- a/packages/compiler/src/library/ir-policy.ts +++ b/packages/compiler/src/library/ir-policy.ts @@ -51,6 +51,7 @@ const LIB_MODE_REFUSED_PREFIXES: readonly [string, string][] = [ ["dns.", "the node:dns surface"], ["tls.", "the node:tls surface"], ["crypto.cipherNew", "crypto.createCipheriv and createDecipheriv"], + ["webcrypto.", "WebCrypto (crypto.subtle)"], ["fs.watch", "fs.watch"], ["watcher.", "fs.watch"], ["test.", "the node:test surface"], diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts index c0d89bbbf..7d00acbc8 100644 --- a/packages/compiler/src/native/cli.ts +++ b/packages/compiler/src/native/cli.ts @@ -100,6 +100,7 @@ async function main(): Promise { nodeTest: false, tls: false, cipher: false, + webcrypto: false, tlsCa: false, }, options.optimization ?? "release", diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts index 18635c8c7..e43ec6085 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -153,6 +153,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures { "tls", "tlsCa", "cipher", + "webcrypto", ].every((name) => typeof features[name] === "boolean"); } diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts index 9047f3376..52beef8f9 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -103,6 +103,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")( tls: false, tlsCa: false, cipher: false, + webcrypto: false, nativeFetch: false, netEffective: false, httpEffective: false, diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index a167254c2..809612806 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5638,6 +5638,20 @@ "code": "SC2020", "note": "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)" }, + { + "id": "stdlib.webcrypto.CryptoKey", + "kind": "stdlib", + "name": "CryptoKey", + "status": "static", + "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + }, + { + "id": "stdlib.webcrypto.subtle", + "kind": "stdlib", + "name": "globalThis.crypto.subtle", + "status": "static", + "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + }, { "id": "syntax.async-generators", "kind": "syntax", diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json new file mode 100644 index 000000000..6b7a65b24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6291-webcrypto-hmac-sign-verify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6291-webcrypto-hmac-sign-verify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json new file mode 100644 index 000000000..a1b096f32 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6292-webcrypto-hmac-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6292-webcrypto-hmac-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json new file mode 100644 index 000000000..7200349f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6293-webcrypto-digest.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6293-webcrypto-digest.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json new file mode 100644 index 000000000..c68b4b49c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6294-webcrypto-js-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6294-webcrypto-js-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json new file mode 100644 index 000000000..286b2c4bf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6295-jose-hs256/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6295-jose-hs256/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json new file mode 100644 index 000000000..05d9557e7 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6296-array-position-patterns.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6296-array-position-patterns.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json new file mode 100644 index 000000000..3c3818490 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6297-library-runtime-idioms.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6297-library-runtime-idioms.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json new file mode 100644 index 000000000..5f9d95858 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/webcrypto-refusals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/webcrypto-refusals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json new file mode 100644 index 000000000..af889405d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-hmac.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-hmac.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 54e789624..8f63e3ea9 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -112,6 +112,7 @@ const optional = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6b5052482..f8d1264eb 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -102,6 +102,7 @@ const OPTIONAL = [ ["scr_tls_ca.c", "tlsCaEffective"], ["scr_tls.c", "tlsEffective"], ["scr_cipher.c", "cipher"], + ["scr_webcrypto.c", "webcrypto"], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index 70fa94dd2..edefb9660 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -1,4 +1,5 @@ -/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c). Included by +/* scriptc runtime — node:crypto (scr_lib.c, scr_crypto_async.c) and the + * WebCrypto HMAC slice (scr_webcrypto.c). Included by * scr_runtime.h, which says where new declarations go. */ #ifndef SCR_CRYPTO_H #define SCR_CRYPTO_H @@ -189,6 +190,22 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t unsigned char out[64]); size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); +/* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── + * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and + * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * call's argument vector as one dyn array (borrowed) and answers a +1 + * promise that every argument or validation failure rejects with Node's + * TypeError or DOMException; algorithms and formats outside the slice + * reject with a coded SC2020 Error. importKey settles at once with a + * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and + * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key + * answers `value instanceof CryptoKey`. */ +ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_sign(ScrDyn *args); +ScrPromise *scr_webcrypto_verify(ScrDyn *args); +ScrPromise *scr_webcrypto_digest(ScrDyn *args); +bool scr_webcrypto_is_crypto_key(const ScrDyn *value); + /* The composed `new crypto.X509Certificate(data).fingerprint` read (the * handle never materializes): the SHA-1 of the DER, uppercase * colon-separated — PEM or raw-DER input; anything else throws Node's diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index bccd8d55c..ca37aedc5 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -174,6 +174,7 @@ typedef enum { SCR_DYNH_HASH, /* node:crypto Hash (scr_lib.c) */ SCR_DYNH_HMAC, /* node:crypto Hmac (scr_lib.c) */ SCR_DYNH_ITERATOR_HELPER, /* an iterator helper object over a checked iterator (scr_json.c) */ + SCR_DYNH_CRYPTO_KEY, /* WebCrypto CryptoKey (scr_webcrypto.c) */ SCR_DYNH_COUNT, } ScrDynHandleTag; diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c new file mode 100644 index 000000000..546b24151 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto.c @@ -0,0 +1,829 @@ +/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), + * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values + * importKey answers. + * + * This unit is only the WebCrypto layer, modeled on Node 24's + * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument + * conversion, algorithm normalization, the CryptoKey handle, promise + * wrapping and Node's error shapes. The MACs and digests themselves come + * from node:crypto's digest core (scr_crypto_hmac_raw and + * scr_crypto_digest_raw in scr_lib.c), and results settle through the + * deferred crypto queue that node:crypto's callback forms use, so sign, + * verify and digest settle on a later loop turn like Node's thread-pool + * jobs while importKey settles at once. + * + * Every argument and validation failure rejects the returned promise. + * Algorithms Node recognizes but this slice does not implement (RSA, + * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded + * SC2020 Error instead of silently diverging. The compiler links the unit + * only into programs that call these methods (the webcrypto feature). */ +#include "scr_runtime.h" + +#include +#include +#include +#include +#include + +/* ── algorithm registry ─────────────────────────────────────────────── + * The names Node 24 registers per operation (util.js's + * kAlgorithmDefinitions with the conditional algorithms its OpenSSL + * build enables), matched case-insensitively. Only HMAC and the SHA-1/ + * SHA-2 digests are implemented. */ +static const char *const scr_wc_import_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", + "ECDH", "ECDSA", "Ed25519", "Ed448", "HKDF", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "PBKDF2", "RSA-OAEP", "RSA-PSS", + "RSASSA-PKCS1-v1_5", "X25519", "X448", +}; +static const char *const scr_wc_sign_names[] = { + "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", + "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", +}; +static const char *const scr_wc_digest_names[] = { + "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", + "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", + "TurboSHAKE256", +}; + +#define SCR_WC_COUNT(names) (sizeof(names) / sizeof((names)[0])) + +/* The implemented hashes: WebCrypto name and node:crypto digest name. */ +static const struct { + const char *name; + const char *native; +} scr_wc_hashes[] = { + {"SHA-1", "sha1"}, + {"SHA-256", "sha256"}, + {"SHA-384", "sha384"}, + {"SHA-512", "sha512"}, +}; + +static bool scr_wc_ascii_ieq(const ScrStr *s, const char *name) { + size_t n = strlen(name); + if (s->len != n) return false; + for (size_t i = 0; i < n; i++) { + unsigned char a = (unsigned char)s->data[i], b = (unsigned char)name[i]; + if (a >= 'a' && a <= 'z') a = (unsigned char)(a - 32); + if (b >= 'a' && b <= 'z') b = (unsigned char)(b - 32); + if (a != b) return false; + } + return true; +} + +/* The canonical registered spelling of `name`, or NULL. */ +static const char *scr_wc_lookup(const char *const *names, size_t count, const ScrStr *name) { + for (size_t i = 0; i < count; i++) + if (scr_wc_ascii_ieq(name, names[i])) return names[i]; + return NULL; +} + +static const char *scr_wc_native_hash(const char *name) { + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_hashes); i++) + if (strcmp(scr_wc_hashes[i].name, name) == 0) return scr_wc_hashes[i].native; + return NULL; +} + +/* ── errors ────────────────────────────────────────────────────────── */ + +static const char *const scr_wc_ordinals[] = {"1st", "2nd", "3rd", "4th", "5th"}; + +static void scr_wc_type_error(const char *code, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +static void scr_wc_type_error(const char *code, const char *fmt, ...) { + char message[512]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(message, sizeof message, fmt, ap); + va_end(ap); + if (n < 0) n = 0; + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, (size_t)n, code); +} + +/* An operation or algorithm Node supports that this slice does not. */ +static void scr_wc_unsupported(const char *method, const char *what) { + char message[256]; + int n = snprintf(message, sizeof message, "crypto.subtle.%s with %s is not supported yet", method, + what); + if ((size_t)n >= sizeof message) n = (int)sizeof message - 1; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)n, "SC2020"); +} + +static void scr_wc_unsupported_algorithm(const char *method, const char *name) { + char what[96]; + snprintf(what, sizeof what, "the %s algorithm", name); + scr_wc_unsupported(method, what); +} + +/* ── Web IDL conversions ────────────────────────────────────────────── + * Each answers NULL/false with the TypeError pending. `prefix` is + * "Failed to execute '' on 'SubtleCrypto'". */ + +static bool scr_wc_is_object(const ScrDyn *v) { + switch (v->kind) { + case SCR_DYN_UNDEF: + case SCR_DYN_NULL: + case SCR_DYN_BOOL: + case SCR_DYN_NUM: + case SCR_DYN_STR: + case SCR_DYN_BIGINT: + case SCR_DYN_SYMBOL: + return false; + default: + return true; + } +} + +/* A typed reference capsule as its plain view; other values retained. */ +static ScrDyn *scr_wc_plain(ScrDyn *v) { + if (v->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(v); + return scr_dyn_retain(v); +} + +/* BufferSource: the bytes of a TypedArray, DataView, Buffer or + * ArrayBuffer as a u8 view (+1). */ +static ScrBytes *scr_wc_buffer_source(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return NULL; + ScrBytes *view = NULL; + if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + view = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s argument is not instance of ArrayBuffer, Buffer, TypedArray, or " + "DataView.", + prefix, scr_wc_ordinals[index]); + } + scr_dyn_release(plain); + return view; +} + +/* DOMString conversion (+1). */ +static ScrStr *scr_wc_dom_string(const ScrDyn *value) { return scr_dyn_string_coerce_js(value); } + +static const char *const scr_wc_key_formats[] = { + "raw", "raw-public", "raw-seed", "raw-secret", "raw-private", "pkcs8", "spki", "jwk", +}; + +/* KeyFormat: the canonical enum spelling, or NULL. */ +static const char *scr_wc_key_format(const char *prefix, ScrDyn *value) { + ScrStr *s = scr_wc_dom_string(value); + if (!s) return NULL; + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_key_formats); i++) { + if (s->len == strlen(scr_wc_key_formats[i]) && + memcmp(s->data, scr_wc_key_formats[i], s->len) == 0) { + scr_str_release(s); + return scr_wc_key_formats[i]; + } + } + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: 1st argument '%.*s' is not a valid enum value of type KeyFormat.", prefix, + (int)(s->len > 200 ? 200 : s->len), s->data); + scr_str_release(s); + return NULL; +} + +/* Node's canonical usage order; the bit is the index. */ +static const char *const scr_wc_usages[] = { + "encrypt", "decrypt", "sign", "verify", + "deriveKey", "deriveBits", "wrapKey", "unwrapKey", + "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", +}; +enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; + +/* sequence over an array: the usage mask (-1 on failure). */ +static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) return -1; + if (plain->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s argument cannot be converted to sequence.", + prefix, scr_wc_ordinals[index]); + scr_dyn_release(plain); + return -1; + } + long mask = 0; + double n = scr_dyn_arr_len(plain); + for (double i = 0; i < n; i++) { + ScrDyn *item = scr_dyn_arr_at(plain, i); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) { + scr_dyn_release(plain); + return -1; + } + size_t k = 0; + while (k < SCR_WC_COUNT(scr_wc_usages) && + !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) + k++; + if (k == SCR_WC_COUNT(scr_wc_usages)) { + scr_wc_type_error("ERR_INVALID_ARG_VALUE", + "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", + prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), + s->data); + scr_str_release(s); + scr_dyn_release(plain); + return -1; + } + scr_str_release(s); + mask |= 1L << k; + } + scr_dyn_release(plain); + return mask; +} + +/* ── algorithm normalization ────────────────────────────────────────── + * normalizeAlgorithm(algorithm, op) for the members this slice reads: + * the canonical name, and for HMAC imports the hash and length. */ +typedef struct { + const char *name; /* canonical registered name */ + const char *hash; /* HMAC import: the normalized hash name */ + bool has_length; + double length; +} ScrWcAlgorithm; + +static const char scr_wc_normalize_prefix[] = "Failed to normalize algorithm"; + +static void scr_wc_missing_member(const char *dict, const char *member) { + scr_wc_type_error("ERR_MISSING_OPTION", + "%s: passed algorithm cannot be converted to '%s' because '%s' is required in " + "'%s'.", + scr_wc_normalize_prefix, dict, member, dict); +} + +/* Reads `key` off an algorithm dictionary (+1; undefined when absent). */ +static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { + if (dict->kind != SCR_DYN_OBJ) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *v = scr_dyn_obj_read(dict, key, strlen(key)); + return v ? v : scr_dyn_retain(scr_dyn_undefined()); +} + +/* The name an AlgorithmIdentifier carries (+1), with the Algorithm + * dictionary's missing-name TypeError. `dict` names the dictionary the + * value converts to. */ +static ScrStr *scr_wc_algorithm_name(ScrDyn *identifier, const char *dict) { + if (!scr_wc_is_object(identifier)) return scr_wc_dom_string(identifier); + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return NULL; + ScrDyn *name = scr_wc_member(plain, "name"); + scr_dyn_release(plain); + ScrStr *out = NULL; + if (name->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "name"); + else out = scr_wc_dom_string(name); + scr_dyn_release(name); + return out; +} + +/* The digest a HashAlgorithmIdentifier names: canonical name, or NULL + * with NotSupportedError / the SC2020 refusal pending. */ +static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *method) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = scr_wc_lookup(scr_wc_digest_names, SCR_WC_COUNT(scr_wc_digest_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_native_hash(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + return canonical; +} + +/* HmacImportParams over an object: hash (required, a SHA name), then the + * optional [EnforceRange] unsigned long length. */ +static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { + if (!scr_wc_is_object(identifier)) { + scr_wc_missing_member("HmacImportParams", "hash"); + return false; + } + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *hash = scr_wc_member(plain, "hash"); + bool ok = true; + if (hash->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member("HmacImportParams", "hash"); + ok = false; + } + ScrDyn *hash_value = NULL; + if (ok) { + /* HashAlgorithmIdentifier converts to an object or a DOMString, then + * ensureSHA reads the name: a string, or the object's name member. */ + ScrStr *hash_name = NULL; + if (scr_wc_is_object(hash)) { + hash_value = scr_dyn_retain(hash); + ScrDyn *hp = scr_wc_plain(hash); + ScrDyn *member = hp ? scr_wc_member(hp, "name") : NULL; + if (hp) scr_dyn_release(hp); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) hash_name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + } else { + hash_name = scr_wc_dom_string(hash); + if (!hash_name) ok = false; + else hash_value = scr_dyn_new_str(hash_name); + } + bool sha = hash_name && hash_name->len >= 3 && + (hash_name->data[0] | 32) == 's' && (hash_name->data[1] | 32) == 'h' && + (hash_name->data[2] | 32) == 'a'; + if (hash_name) scr_str_release(hash_name); + if (ok && !sha) { + scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + ok = false; + } + } + if (ok) { + ScrDyn *length = scr_wc_member(plain, "length"); + if (length->kind != SCR_DYN_UNDEF) { + double x; + if (!scr_dyn_number_coerce_js(length, &x)) ok = false; + else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: length in passed algorithm is not a finite number.", + scr_wc_normalize_prefix); + ok = false; + } else { + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > 4294967295.0) { + scr_wc_type_error("ERR_OUT_OF_RANGE", + "%s: length in passed algorithm is outside the expected range of 0 to " + "4294967295.", + scr_wc_normalize_prefix); + ok = false; + } else if (x == 0) { + scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); + ok = false; + } else { + out->has_length = true; + out->length = x; + } + } + } + scr_dyn_release(length); + } + if (ok) { + out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + if (!out->hash) ok = false; + } + if (hash_value) scr_dyn_release(hash_value); + scr_dyn_release(hash); + scr_dyn_release(plain); + return ok; +} + +/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with + * the error pending. Only HMAC parameters are read; any other recognized + * algorithm is refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, + bool import, ScrWcAlgorithm *out) { + memset(out, 0, sizeof *out); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + out->name = scr_wc_lookup(names, count, name); + scr_str_release(name); + if (!out->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + return true; +} + +/* ── CryptoKey ───────────────────────────────────────────────────────── + * A secret HMAC key: the bytes, the hash, the bit length, the usage mask + * and the extractable flag. The algorithm and usages getters answer the + * same object on every read (Node caches its clones), built on first + * read. The bytes are cleared when the last reference goes. */ +typedef struct ScrCryptoKey { + size_t rc; + const char *hash; /* WebCrypto hash name */ + double length; /* bits */ + unsigned usages; + bool extractable; + ScrDyn *algorithm; /* cached getter results, or NULL */ + ScrDyn *usage_list; + size_t len; + unsigned char data[]; +} ScrCryptoKey; + +static void *scr_crypto_key_retain(void *p) { + ((ScrCryptoKey *)p)->rc++; + return p; +} + +static void scr_crypto_key_release(void *p) { + ScrCryptoKey *key = p; + if (!key || --key->rc != 0) return; + volatile unsigned char *bytes = key->data; + for (size_t i = 0; i < key->len; i++) bytes[i] = 0; + if (key->algorithm) scr_dyn_release(key->algorithm); + if (key->usage_list) scr_dyn_release(key->usage_list); + free(key); +} + +static ScrCryptoKey *scr_crypto_key_of(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE || value->v.handle.tag != SCR_DYNH_CRYPTO_KEY) return NULL; + return value->v.handle.ptr; +} + +bool scr_webcrypto_is_crypto_key(const ScrDyn *value) { + if (value->kind == SCR_DYN_TYPED_REF) return false; + return scr_crypto_key_of(value) != NULL; +} + +static ScrDyn *scr_wc_cstr(const char *s) { + ScrStr *str = scr_str_new(s, strlen(s)); + ScrDyn *out = scr_dyn_new_str(str); + scr_str_release(str); + return out; +} + +static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { + if (!key->algorithm) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + ScrDyn *algorithm = scr_dyn_new_obj(); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + key->algorithm = algorithm; + } + return scr_dyn_retain(key->algorithm); +} + +static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { + if (!key->usage_list) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + key->usage_list = list; + } + return scr_dyn_retain(key->usage_list); +} + +static bool scr_wc_name_is(const char *name, size_t len, const char *literal) { + return len == strlen(literal) && memcmp(name, literal, len) == 0; +} + +static ScrDyn *scr_crypto_key_get(void *h, const char *name, size_t len) { + ScrCryptoKey *key = h; + if (scr_wc_name_is(name, len, "type")) return scr_wc_cstr("secret"); + if (scr_wc_name_is(name, len, "extractable")) return scr_dyn_new_bool(key->extractable); + if (scr_wc_name_is(name, len, "algorithm")) return scr_crypto_key_algorithm(key); + if (scr_wc_name_is(name, len, "usages")) return scr_crypto_key_usages(key); + return NULL; +} + +/* The four members are prototype getters without setters: an assignment + * in strict code throws Node's TypeError. */ +static bool scr_crypto_key_set(void *h, const char *name, size_t len, const ScrDyn *value) { + (void)h; + (void)value; + if (!scr_wc_name_is(name, len, "type") && !scr_wc_name_is(name, len, "extractable") && + !scr_wc_name_is(name, len, "algorithm") && !scr_wc_name_is(name, len, "usages")) + return false; + char message[128]; + int n = snprintf(message, sizeof message, + "Cannot set property %.*s of # which has only a getter", (int)len, + name); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n); + return true; +} + +static ScrDyn *scr_crypto_key_invoke(void *h, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + (void)h; + (void)self; + (void)args; + (void)argc; + if (strcmp(method, "toString") == 0) return scr_wc_cstr("[object CryptoKey]"); + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, what); + scr_jb_puts(&b, " is not a function"); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return NULL; +} + +/* key[Symbol.toStringTag] answers "CryptoKey". */ +static ScrDyn *scr_crypto_key_symbol_get(void *h, const ScrDyn *key) { + (void)h; + if (key->kind != SCR_DYN_SYMBOL || !key->v.symbol.description) return NULL; + ScrStr *description = key->v.symbol.description(key->v.symbol.value); + bool tag = description && description->len == 18 && + memcmp(description->data, "Symbol.toStringTag", 18) == 0; + if (description) scr_str_release(description); + return tag ? scr_wc_cstr("CryptoKey") : NULL; +} + +/* No inspect_view: Node's custom inspector prints the members one level + * deeper than a plain object would, which the shared handle inspection + * cannot reproduce, so util.inspect refuses a CryptoKey ('not supported + * yet') rather than printing a different shape. */ +static const ScrDynHandleOps scr_crypto_key_ops = { + .cls = "CryptoKey", + .retain = scr_crypto_key_retain, + .release = scr_crypto_key_release, + .invoke = scr_crypto_key_invoke, + .get = scr_crypto_key_get, + .set = scr_crypto_key_set, + .json_empty_object = true, + .to_string_tag = "CryptoKey", + .symbol_get = scr_crypto_key_symbol_get, +}; + +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, + double length, unsigned usages, bool extractable) { + static SCR_TL bool installed; + if (!installed) { + installed = true; + scr_dyn_handle_install(SCR_DYNH_CRYPTO_KEY, &scr_crypto_key_ops); + } + ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); + if (!key) scr_trap("scriptc: out of memory\n"); + key->rc = 1; + key->hash = hash; + key->length = length; + key->usages = usages; + key->extractable = extractable; + key->algorithm = NULL; + key->usage_list = NULL; + key->len = len; + if (len) memcpy(key->data, data, len); + ScrDyn *box = scr_dyn_new_handle(key, SCR_DYNH_CRYPTO_KEY); + scr_crypto_key_release(key); + return box; +} + +/* ── the operations ─────────────────────────────────────────────────── + * Each takes its argument vector as one dyn array (borrowed) and answers + * a +1 promise. */ + +static ScrPromise *scr_wc_rejected(void) { + ScrPromise *p = scr_promise_new(); + scr_promise_reject_pending(p); + return p; +} + +/* The argument count check every method starts with (after the receiver + * check the compiler's call shape already guarantees). */ +static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { + size_t argc = (size_t)scr_dyn_arr_len(args); + if (argc >= required) return true; + scr_wc_type_error("ERR_MISSING_ARGS", "%s: %zu arguments required, but only %zu present.", prefix, + required, argc); + return false; +} + +typedef struct { + ScrDyn *v[5]; +} ScrWcArgs; + +static void scr_wc_args_take(ScrWcArgs *a, const ScrDyn *args, size_t n) { + for (size_t i = 0; i < n; i++) a->v[i] = scr_dyn_arr_at(args, (double)i); +} + +static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { + for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); +} + +/* importKey(format, keyData, algorithm, extractable, keyUsages): Node's + * conversion order, then macImportKey's checks and importKeySync's empty + * usage check. */ +static ScrDyn *scr_wc_import(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrDyn *result = NULL; + ScrBytes *data = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + bool jwk = strcmp(format, "jwk") == 0; + /* The JsonWebKey dictionary is not implemented; that format is refused + * after the remaining arguments convert. */ + if (!jwk) { + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + } + if (!scr_wc_is_object(a.v[2])) { + ScrStr *s = scr_wc_dom_string(a.v[2]); + if (!s) goto done; + scr_str_release(s); + } + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm("importKey", algorithm.name); + goto done; + } + if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { + scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); + goto done; + } + if (jwk) { + scr_wc_unsupported("importKey", "the 'jwk' key format"); + goto done; + } + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + char message[96]; + snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); + scr_throw_domex("NotSupportedError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + double length = (double)len * 8; + unsigned char last = view->data[len - 1]; + if (algorithm.has_length) { + double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); + if (bytes != (double)len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + /* A length that is not a whole number of bytes clears the unused + * low bits of the final byte (truncateToBitLength). */ + int extra = (int)fmod(algorithm.length, 8); + if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); + length = algorithm.length; + } + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + goto done; + } + result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, + extractable); + scr_crypto_key_of(result)->data[len - 1] = last; + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 5); + return result; +} + +ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { + ScrDyn *key = scr_wc_import(args); + if (!key) return scr_wc_rejected(); + return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* sign(algorithm, key, data) / verify(algorithm, key, signature, data): + * the HMAC over `data`, or NULL with the error pending. *mac_len is the + * digest length. */ +static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char mac[64], + size_t *mac_len, ScrBytes **signature) { + const char *prefix = verify ? "Failed to execute 'verify' on 'SubtleCrypto'" + : "Failed to execute 'sign' on 'SubtleCrypto'"; + const char *method = verify ? "verify" : "sign"; + size_t n = verify ? 4 : 3; + if (!scr_wc_required(prefix, args, n)) return false; + ScrWcArgs a; + scr_wc_args_take(&a, args, n); + bool ok = false; + ScrBytes *data = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (verify) { + *signature = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!*signature) goto done; + } + data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); + if (!data) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, + &algorithm)) + goto done; + if (strcmp(algorithm.name, "HMAC") != 0) { + scr_wc_unsupported_algorithm(method, algorithm.name); + goto done; + } + if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + *mac_len = scr_crypto_hmac_raw(scr_wc_native_hash(key->hash), key->data, key->len, view->data, + (size_t)scr_bytes_byte_len(view), mac); + } + ok = true; +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, n); + return ok; +} + +ScrPromise *scr_webcrypto_sign(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + if (!scr_wc_sign_verify(args, false, mac, &mac_len, NULL)) return scr_wc_rejected(); + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)mac_len); + memcpy(out->data, mac, mac_len); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +/* The verify result's adapter: the deferred value is a dyn boolean. */ +static void scr_wc_settle_verified(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + ScrPromise *p; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + ScrDyn *verified = (ScrDyn *)value; + scr_promise_fulfill_bool(p, scr_dyn_truthy(verified)); + scr_dyn_release(verified); +} + +ScrPromise *scr_webcrypto_verify(ScrDyn *args) { + unsigned char mac[64]; + size_t mac_len = 0; + ScrBytes *signature = NULL; + if (!scr_wc_sign_verify(args, true, mac, &mac_len, &signature)) { + if (signature) scr_bytes_release(signature); + return scr_wc_rejected(); + } + bool equal; + { + ScrBytes *view = signature; + SCR_BYTES_SNAPSHOT(view); + /* A signature of another length is simply not valid; equal lengths + * compare in constant time. */ + size_t sig_len = (size_t)scr_bytes_byte_len(view); + equal = sig_len == mac_len; + unsigned char diff = 0; + for (size_t i = 0; equal && i < mac_len; i++) diff |= (unsigned char)(view->data[i] ^ mac[i]); + equal = equal && diff == 0; + } + scr_bytes_release(signature); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(scr_dyn_new_bool(equal), NULL, scr_crypto_promise_callback(p), + scr_wc_settle_verified); + return p; +} + +/* digest(algorithm, data). */ +ScrPromise *scr_webcrypto_digest(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'digest' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrBytes *data = NULL; + ScrBytes *out = NULL; + if (!scr_wc_is_object(a.v[0])) { + ScrStr *s = scr_wc_dom_string(a.v[0]); + if (!s) goto done; + scr_str_release(s); + } + data = scr_wc_buffer_source(prefix, 1, a.v[1]); + if (!data) goto done; + const char *hash = scr_wc_digest_algorithm(a.v[0], "digest"); + if (!hash) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + unsigned char digest[64]; + size_t len = scr_crypto_digest_raw(scr_wc_native_hash(hash), view->data, + (size_t)scr_bytes_byte_len(view), digest); + out = scr_bytes_new(SCR_BYTES_U8, (double)len); + memcpy(out->data, digest, len); + } +done: + if (data) scr_bytes_release(data); + scr_wc_args_drop(&a, 2); + if (!out) return scr_wc_rejected(); + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(out, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} diff --git a/tests/corpus/6291-webcrypto-hmac-sign-verify.ts b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts new file mode 100644 index 000000000..ddb480a4d --- /dev/null +++ b/tests/corpus/6291-webcrypto-hmac-sign-verify.ts @@ -0,0 +1,94 @@ +// WebCrypto HMAC through the crypto global: importKey('raw') for SHA-1 and +// SHA-2 keys, sign and verify (a wrong or short signature answers false), +// the CryptoKey members, instanceof CryptoKey, the key sources importKey +// and sign accept, and that results settle asynchronously. +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("header.payload"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const key = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], +); +console.log(key.type, key.extractable, key.usages.join(",")); +const algorithm = key.algorithm as HmacKeyAlgorithm; +console.log(algorithm.name, algorithm.length, algorithm.hash.name); +console.log(key instanceof CryptoKey, key.algorithm === key.algorithm, key.usages === key.usages); + +const signature = await crypto.subtle.sign("HMAC", key, data); +console.log(signature.byteLength, hex(signature)); +console.log(await crypto.subtle.verify("HMAC", key, signature, data)); +console.log(await crypto.subtle.verify({ name: "HMAC" }, key, signature, encoder.encode("header.payloaD"))); +const tampered = new Uint8Array(signature); +tampered[0] ^= 1; +console.log(await crypto.subtle.verify("HMAC", key, tampered, data)); +console.log(await crypto.subtle.verify("HMAC", key, signature.slice(0, 16), data)); +console.log(await crypto.subtle.verify("HMAC", key, new Uint8Array(0), data)); + +for (const hash of ["SHA-1", "SHA-384", "SHA-512"]) { + const k = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash }, true, ["sign"]); + const a = k.algorithm as HmacKeyAlgorithm; + const mac = await crypto.subtle.sign({ name: "hmac" }, k, data); + console.log(hash, a.hash.name, a.length, k.extractable, k.usages.join(","), mac.byteLength, hex(mac)); +} + +// Names normalize case-insensitively; usages come back in Node's order. +const lower = await crypto.subtle.importKey( + "raw", + secret, + { name: "hmac", hash: { name: "sha-256" } }, + true, + ["verify", "sign"], +); +const lowerAlgorithm = lower.algorithm as HmacKeyAlgorithm; +console.log(lowerAlgorithm.name, lowerAlgorithm.hash.name, lower.usages.join(",")); + +// An explicit length in bits must match the key bytes. +const sized = await crypto.subtle.importKey( + "raw", + secret, + { name: "HMAC", hash: "SHA-256", length: secret.length * 8 }, + false, + ["verify"], +); +console.log((sized.algorithm as HmacKeyAlgorithm).length, sized.usages.join(",")); + +// Key and data sources: ArrayBuffer, Buffer, DataView and subarray views. +const fromBuffer = await crypto.subtle.importKey( + "raw", + secret.slice().buffer, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(hex(await crypto.subtle.sign("HMAC", fromBuffer, Buffer.from("header.payload"))) === hex(signature)); +const padded = encoder.encode("xxheader.payloadyy"); +console.log(hex(await crypto.subtle.sign("HMAC", key, padded.subarray(2, 16))) === hex(signature)); +console.log(hex(await crypto.subtle.sign("HMAC", key, new DataView(data.buffer))) === hex(signature)); + +// The data is read when sign is called. +const mutable = encoder.encode("header.payload"); +const pending = crypto.subtle.sign("HMAC", key, mutable); +mutable[0] = 0; +console.log(hex(await pending) === hex(signature)); + +// Results settle after the synchronous code that started them. +const order: string[] = []; +const signed = crypto.subtle.sign("HMAC", key, data).then(() => { + order.push("sign"); +}); +const imported = crypto.subtle + .importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]) + .then(() => { + order.push("importKey"); + }); +order.push("sync"); +await Promise.all([signed, imported]); +console.log(order.join(",")); diff --git a/tests/corpus/6292-webcrypto-hmac-errors.ts b/tests/corpus/6292-webcrypto-hmac-errors.ts new file mode 100644 index 000000000..95e33ae84 --- /dev/null +++ b/tests/corpus/6292-webcrypto-hmac-errors.ts @@ -0,0 +1,83 @@ +// WebCrypto HMAC failures reject with Node's TypeErrors (argument count, +// enum values, buffer sources, CryptoKey arguments, required members) and +// DOMExceptions (usages, key length, algorithm names, key access). +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-webcrypto-secret"); +const data = encoder.encode("payload"); + +function report(label: string, error: Error): void { + if (error instanceof DOMException) { + const dom = error as DOMException; + console.log(label, dom.name, dom.code, dom.message, "DOMException"); + } else { + const coded = error as Error & { code?: string }; + console.log(label, coded.name, coded.code, coded.message); + } +} + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + report(label, e as Error); + } +} + +async function attemptVerify(label: string, run: () => Promise): Promise { + try { + console.log(label, "resolved", await run()); + } catch (e) { + report(label, e as Error); + } +} + +const signOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const verifyOnly = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-384" }, false, ["verify"]); + +await attempt("usage", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "encrypt"]), +); +await attempt("empty usages", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, []), +); +await attempt("zero-length key", () => + crypto.subtle.importKey("raw", new Uint8Array(0), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("length mismatch", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 64 }, false, ["sign"]), +); +await attempt("length zero", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256", length: 0 }, false, ["sign"]), +); +await attempt("missing hash", () => + crypto.subtle.importKey("raw", secret, "HMAC", false, ["sign"]), +); +await attempt("md5 hash", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "MD5" }, false, ["sign"]), +); +await attempt("unknown sha", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-999" }, false, ["sign"]), +); +await attempt("unknown algorithm", () => + crypto.subtle.importKey("raw", secret, { name: "NOPE", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("pkcs8 format", () => + crypto.subtle.importKey("pkcs8", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +await attempt("string key", () => + crypto.subtle.importKey("raw", "secret" as unknown as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); + +await attempt("sign with verify key", () => crypto.subtle.sign("HMAC", verifyOnly, data)); +await attemptVerify("verify with sign key", () => crypto.subtle.verify("HMAC", signOnly, new Uint8Array(32), data)); +await attempt("sign without key", () => + crypto.subtle.sign("HMAC", secret as unknown as CryptoKey, data), +); +await attempt("sign unknown algorithm", () => crypto.subtle.sign("HMAC-SHA256", signOnly, data)); +await attempt("sign string data", () => + crypto.subtle.sign("HMAC", signOnly, "payload" as unknown as BufferSource), +); +await attemptVerify("verify string signature", () => + crypto.subtle.verify("HMAC", verifyOnly, "sig" as unknown as BufferSource, data), +); diff --git a/tests/corpus/6293-webcrypto-digest.ts b/tests/corpus/6293-webcrypto-digest.ts new file mode 100644 index 000000000..ae107c6f0 --- /dev/null +++ b/tests/corpus/6293-webcrypto-digest.ts @@ -0,0 +1,35 @@ +// crypto.subtle.digest over the SHA-1 and SHA-2 hashes: string and object +// algorithm identifiers, typed-array, Buffer and ArrayBuffer input, and +// Node's rejections for unknown names and non-buffer data. +const encoder = new TextEncoder(); +const input = encoder.encode("scriptc digest input"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +for (const name of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + const digest = await crypto.subtle.digest(name, input); + console.log(name, digest.byteLength, hex(digest)); +} +console.log(hex(await crypto.subtle.digest({ name: "sha-256" }, Buffer.from("scriptc digest input")))); +console.log(hex(await crypto.subtle.digest("SHA-256", input.slice().buffer))); +console.log(hex(await crypto.subtle.digest("SHA-256", new Uint8Array(0)))); + +async function attempt(label: string, run: () => Promise): Promise { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + if (e instanceof DOMException) { + const error = e as DOMException; + console.log(label, error.name, error.code, error.message, "DOMException"); + } else { + const error = e as Error & { code?: string }; + console.log(label, error.name, error.code, error.message); + } + } +} + +await attempt("md5", () => crypto.subtle.digest("MD5", input)); +await attempt("string data", () => crypto.subtle.digest("SHA-256", "text" as unknown as BufferSource)); diff --git a/tests/corpus/6294-webcrypto-js-values.mjs b/tests/corpus/6294-webcrypto-js-values.mjs new file mode 100644 index 000000000..93071b39d --- /dev/null +++ b/tests/corpus/6294-webcrypto-js-values.mjs @@ -0,0 +1,52 @@ +// WebCrypto from JavaScript: keys and algorithms carried as dynamic +// values, the CryptoKey brand checks libraries use (Symbol.toStringTag, +// Object.prototype.toString and instanceof), JSON.stringify of a key, and +// rejections for missing arguments and non-key values. +const encoder = new TextEncoder(); +const secret = encoder.encode("dynamic-secret"); +const table = { + HS256: { name: "HMAC", hash: "SHA-256" }, + HS512: { name: "HMAC", hash: "SHA-512" }, +}; + +async function importFor(alg, usage) { + const params = table[alg]; + return crypto.subtle.importKey("raw", secret, params, false, [usage]); +} + +const key = await importFor("HS256", "sign"); +console.log(key[Symbol.toStringTag], Object.prototype.toString.call(key), String(key)); +console.log(key instanceof CryptoKey, secret instanceof CryptoKey, {} instanceof CryptoKey); +console.log(JSON.stringify(key), typeof key, key.type, key.algorithm.hash.name, key.usages.includes("sign")); +const sig = new Uint8Array(await crypto.subtle.sign(table.HS256, key, encoder.encode("a.b"))); +console.log(sig.length, Buffer.from(sig).toString("base64url")); +const verifier = await importFor("HS256", "verify"); +let verified = false; +try { + verified = await crypto.subtle.verify(table.HS256, verifier, sig, encoder.encode("a.b")); +} catch {} +console.log(verified); +const other = await importFor("HS512", "verify"); +console.log(await crypto.subtle.verify(table.HS512, other, sig, encoder.encode("a.b"))); + +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("too few", () => crypto.subtle.sign("HMAC", key)); +await attempt("import too few", () => crypto.subtle.importKey("raw", secret, table.HS256, false)); +await attempt("verify too few", () => crypto.subtle.verify("HMAC", key, sig)); +await attempt("digest too few", () => crypto.subtle.digest("SHA-256")); +const lookalike = JSON.parse('{"type":"secret","usages":["sign"]}'); +await attempt("not a key", () => crypto.subtle.sign("HMAC", lookalike, secret)); +await attempt("bad format", () => crypto.subtle.importKey("nope", secret, table.HS256, false, ["sign"])); +await attempt("bad usage", () => crypto.subtle.importKey("raw", secret, table.HS256, false, ["bogus"])); +await attempt("usages not a sequence", () => crypto.subtle.importKey("raw", secret, table.HS256, false, "sign")); +await attempt("algorithm without name", () => crypto.subtle.sign({ hash: "SHA-256" }, key, secret)); +await attempt("hash object without name", () => + crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: {} }, false, ["sign"]), +); diff --git a/tests/corpus/6295-jose-hs256/main.mjs b/tests/corpus/6295-jose-hs256/main.mjs new file mode 100644 index 000000000..253fa66e4 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -0,0 +1,70 @@ +// The real jose@6.2.12 HS256 path: SignJWT and jwtVerify from jose's +// public jose/jwt/sign, jose/jwt/verify and jose/errors entry points, over +// crypto.subtle importKey, sign and verify. node_modules/jose vendors the +// dist/webapi files those entry points load and their declarations, +// unmodified (MIT, LICENSE.md), with a package.json that exports only +// those entry points. +import { SignJWT } from "jose/jwt/sign"; +import { jwtVerify } from "jose/jwt/verify"; +import * as errors from "jose/errors"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-hs256-shared-secret"); +const now = new Date(1_800_000_000_000); +const iat = 1_799_999_000; + +async function sign(claims, alg = "HS256", key = secret) { + const jwt = new SignJWT(claims); + jwt.setProtectedHeader({ alg, typ: "JWT" }); + jwt.setIssuedAt(iat); + jwt.setIssuer("urn:scriptc:issuer"); + jwt.setAudience("urn:scriptc:audience"); + jwt.setSubject("alice"); + return jwt.sign(key); +} + +async function verify(label, token, options = {}, key = secret) { + try { + const { payload, protectedHeader } = await jwtVerify(token, key, { + algorithms: ["HS256"], + currentDate: now, + ...options, + }); + console.log(label, "ok", JSON.stringify(protectedHeader), JSON.stringify(payload)); + } catch (e) { + console.log(label, e.code, e.message, e instanceof errors.JOSEError, e.name, e.claim, e.reason); + } +} + +const token = await sign({ role: "admin", exp: iat + 3600 }); +console.log(token); +await verify("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await verify("bytes token", encoder.encode(token)); +await verify("wrong secret", token, {}, encoder.encode("another-secret")); +const [header, body, signature] = token.split("."); +const forged = Buffer.from(JSON.stringify({ role: "root", sub: "mallory" })).toString("base64url"); +await verify("tampered payload", `${header}.${forged}.${signature}`); +await verify("tampered signature", `${header}.${body}.${signature.slice(0, -2)}AA`); +await verify("malformed", "not-a-jwt"); +await verify("bad signature encoding", `${header}.${body}.***`); +await verify("expired", token, { currentDate: new Date((iat + 7200) * 1000) }); +await verify("clock tolerance", token, { currentDate: new Date((iat + 3610) * 1000), clockTolerance: 30 }); +await verify("not yet valid", await sign({ nbf: iat + 600 })); +await verify("issuer", token, { issuer: "urn:other" }); +await verify("audience", token, { audience: "urn:other" }); +await verify("required claim", token, { requiredClaims: ["jti"] }); +await verify("alg not allowed", token, { algorithms: ["HS512"] }); +await verify("max token age", token, { maxTokenAge: 600 }); + +const hs384 = await sign({ scope: "read" }, "HS384"); +await verify("hs384", hs384, { algorithms: ["HS384"] }); +const hs512 = await sign({ scope: "write" }, "HS512"); +await verify("hs512", hs512, { algorithms: ["HS256", "HS512"] }); +await verify("hs512 as hs256 only", hs512); +await verify("none", `${Buffer.from('{"alg":"none"}').toString("base64url")}.${body}.`, { algorithms: ["none"] }); + +// The same key imported once and reused as a CryptoKey. +const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]); +await verify("crypto key", token, {}, cryptoKey); +const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]); +await verify("wrong key hash", token, {}, signingKey); diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6295-jose-hs256/node_modules/jose/package.json b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json new file mode 100644 index 000000000..bcb353430 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/node_modules/jose/package.json @@ -0,0 +1,17 @@ +{ + "name": "jose", + "version": "6.2.12", + "license": "MIT", + "type": "module", + "sideEffects": false, + "exports": { + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + } + } +} diff --git a/tests/corpus/6296-array-position-patterns.ts b/tests/corpus/6296-array-position-patterns.ts new file mode 100644 index 000000000..8c55b6152 --- /dev/null +++ b/tests/corpus/6296-array-position-patterns.ts @@ -0,0 +1,23 @@ +// Object patterns over arrays that name positions and `length` (jose's +// `const { 0: header, 1: payload, 2: signature, length } = jws.split(".")`): +// each position is the element or undefined past the end, defaults apply +// to missing positions, and `length` is the array's length. +const { 0: header, 1: payload, 2: signature, length } = "aaa.bbb.ccc".split("."); +console.log(header, payload, signature, length); + +const short = "aaa.bbb".split("."); +const { 0: first, 2: third, length: count } = short; +console.log(first, third === undefined, count); + +const { 1: second = "fallback", 3: fourth = "missing" } = short; +console.log(second, fourth); + +function parts(values: number[]): string { + const { 0: a, 1: b, length: n } = values; + return `${a}:${b}:${n}`; +} +console.log(parts([1, 2, 3]), parts([7])); + +let renamed = ""; +for (const { 0: key, 1: value } of [["x", "1"], ["y", "2"]]) renamed += `${key}=${value};`; +console.log(renamed); diff --git a/tests/corpus/6297-library-runtime-idioms.mjs b/tests/corpus/6297-library-runtime-idioms.mjs new file mode 100644 index 000000000..11d5d5e67 --- /dev/null +++ b/tests/corpus/6297-library-runtime-idioms.mjs @@ -0,0 +1,34 @@ +// Runtime idioms from jose's dist build: the Uint8Array base64/hex +// feature probes (absent in Node 24), String.fromCharCode.apply over byte +// chunks and number lists (the btoa fallback), and structuredClone of a +// parameter whose default is `{}` called with differently shaped objects. +console.log(typeof Uint8Array.prototype.toBase64, typeof Uint8Array.prototype.toHex); +console.log(typeof Uint8Array.prototype.setFromBase64, typeof Uint8Array.prototype.setFromHex); + +function encodeBase64(input) { + if (Uint8Array.prototype.toBase64) return input.toBase64(); + const CHUNK_SIZE = 4, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + return btoa(arr.join("")); +} +console.log(encodeBase64(new TextEncoder().encode("hello, base64 chunks"))); +console.log(encodeBase64(new Uint8Array([0, 255, 128, 1, 2]))); + +const bytes = new Uint8Array([104, 105]); +console.log(String.fromCharCode.apply(null, bytes), String.fromCharCode.apply(undefined, [72, 73])); +const untyped = JSON.parse("[79, 75]"); +console.log(String.fromCharCode.apply(null, untyped), String.fromCharCode.apply(null, []).length); + +let store; +class Builder { + constructor(payload = {}) { + (store ||= new WeakMap()).set(this, structuredClone(payload)); + } + claims() { + return JSON.stringify(store.get(this)); + } +} +console.log(new Builder({ role: "admin", exp: 5 }).claims()); +console.log(new Builder({ scope: "read" }).claims()); +console.log(new Builder().claims()); diff --git a/tests/diagnostics/destructuring-fences.ts b/tests/diagnostics/destructuring-fences.ts index b79836920..e42f8a07a 100644 --- a/tests/diagnostics/destructuring-fences.ts +++ b/tests/diagnostics/destructuring-fences.ts @@ -48,8 +48,8 @@ function fromUnion(u: E1 | E2): string { console.log(fromUnion({ kind: "one", b: "s", count: 1 })); const xs = [1, 2, 3]; -const { length: len } = xs; // object patterns over arrays keep the fence -console.log(len); +const { push: append } = xs; // object patterns over arrays read positions and length only +console.log(typeof append); const indexed: Record = { a: 1 }; const { ...allOfIt } = indexed; // index-signature rest needs overflow packing diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts new file mode 100644 index 000000000..d162872fb --- /dev/null +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -0,0 +1,18 @@ +// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, +// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' +// key format and crypto.subtle or its methods taken as values are refused +// by name; the bundled declarations omit the other SubtleCrypto methods +// (node-types/fenced.ts pins their fence under @types/node). +const fromJwk = await crypto.subtle.importKey( + "jwk", + { kty: "oct", k: "c2VjcmV0" }, + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], +); +console.log(fromJwk.type); + +const subtle = crypto.subtle; +console.log(typeof subtle); + +console.log(typeof crypto.subtle.sign); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index b884de3ce..72e53b3f5 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -97,4 +97,11 @@ void callbackified; * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); cipherStream.on("data", () => {}); +/* WebCrypto beyond the HMAC slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside importKey, sign, verify + * and digest fence by name. */ +async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +} +void encryptWithWebCrypto; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/webcrypto-hmac.ts b/tests/fixtures/node-types/webcrypto-hmac.ts new file mode 100644 index 000000000..4b37d7de4 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-hmac.ts @@ -0,0 +1,25 @@ +/* WebCrypto's HMAC slice under @types/node: the crypto global's subtle + * methods and CryptoKey members lower to the same native values as under + * the bundled declarations. @types/node names the dictionary and key + * types in node:crypto's webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const encoder = new TextEncoder(); + const params: webcrypto.HmacImportParams = { name: "HMAC", hash: "SHA-512" }; + const key: webcrypto.CryptoKey = await crypto.subtle.importKey("raw", encoder.encode("node-types"), params, true, [ + "verify", + "sign", + ]); + const algorithm = key.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(key.type, key.extractable, key.usages.join(","), algorithm.name, algorithm.length, algorithm.hash.name); + const signature = await crypto.subtle.sign("HMAC", key, encoder.encode("message")); + console.log(signature.byteLength, Buffer.from(signature).toString("base64")); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("message"))); + console.log(await crypto.subtle.verify("HMAC", key, signature, encoder.encode("massage"))); + const digest = await crypto.subtle.digest("SHA-384", encoder.encode("message")); + console.log(Buffer.from(digest).toString("hex")); + console.log(key instanceof CryptoKey); +} + +void main(); diff --git a/tests/harness/__snapshots__/destructuring-fences.ts.txt b/tests/harness/__snapshots__/destructuring-fences.ts.txt index 65d9c293b..743bb5657 100644 --- a/tests/harness/__snapshots__/destructuring-fences.ts.txt +++ b/tests/harness/__snapshots__/destructuring-fences.ts.txt @@ -38,9 +38,9 @@ destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on destructuring-fences.ts:51:7 - error SC1031: object destructuring of non-record values (the source is array-typed) are not supported yet 50 | const xs = [1, 2, 3]; - 51 | const { length: len } = xs; // object patterns over arrays keep the fence - | ^~~~~~~~~~~~~~~ - 52 | console.log(len); + 51 | const { push: append } = xs; // object patterns over arrays read positions and length only + | ^~~~~~~~~~~~~~~~ + 52 | console.log(typeof append); destructuring-fences.ts:55:9 - error SC1031: rest bindings over index-signature shapes (the undeclared entries would need overflow packing) are not supported yet diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 29e4a2ee9..913b78198 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -210,6 +210,15 @@ fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); 99 | cipherStream.on("data", () => {}); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | // End of the declared-but-not-lowered surface. + 100 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not \ No newline at end of file + hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not + +fenced.ts:104:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet + + 103 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 104 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + | ^~~~~~~~~~~~~~~~~~~~~ + 105 | } + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt new file mode 100644 index 000000000..521edff3c --- /dev/null +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -0,0 +1,26 @@ +webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet + + 6 | const fromJwk = await crypto.subtle.importKey( + 7 | "jwk", + | ^~~~~ + 8 | { kty: "oct", k: "c2VjcmV0" }, + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet + + 14 | + 15 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 16 | console.log(typeof subtle); + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet + + 17 | + 18 | console.log(typeof crypto.subtle.sign); + | ^~~~~~~~~~~~~~~~~~ + 19 | + + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 92ac8f5ab..03d533d62 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1130,3 +1130,65 @@ console.log(count(require("./lib.cjs"))); ); }); }); + +// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and +// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice +// reject with a coded SC2020 Error instead of a Node-shaped success, and a +// JavaScript call with the 'jwk' format meets its named fence when it runs. +// Node's own rejections (unknown names, bad usages) are corpus-pinned +// (6292, 6294); these refusals have no Node twin, so they live here. +describe("WebCrypto refusals (JS lane)", () => { + test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + const r = await compileAndRun( + "webcrypto-unsupported-algorithms", + `const bytes = new TextEncoder().encode("secret"); +async function attempt(label, run) { + try { + await run(); + console.log(label, "resolved"); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +await attempt("rsa", () => + crypto.subtle.importKey("raw", bytes, { name: "RSA-PSS", hash: "SHA-256" }, false, ["verify"]), +); +const key = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +await attempt("ecdsa", () => crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, key, bytes)); +await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); +await attempt("sha3 hmac", () => + crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), +); +const format = JSON.parse('"jwk"'); +await attempt("computed jwk", () => + crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), +); +`, + "mjs", + ); + expect(r.exitCode).toBe(0); + expect(r.stderr).toBe(""); + expect(r.stdout).toBe( + "rsa Error SC2020 crypto.subtle.importKey with the RSA-PSS algorithm is not supported yet\n" + + "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + + "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + ); + }); + + test("a literal 'jwk' import is a named fence where it runs", async () => { + const r = await compileAndRun( + "webcrypto-jwk-literal", + `console.log("before"); +await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +`, + "mjs", + ); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe("before\n"); + expect(r.stderr).toMatch( + /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + ); + }); +}); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index bf57a830a..769ad702d 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -839,6 +839,24 @@ test("node-types: Cipheriv and Decipheriv match Node under @types/node", async ( expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-hmac"); + const entry = join(nodeTypesDir, "webcrypto-hmac.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-hmac"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: declared-but-not-lowered surface fences, naming @types/node", async () => { const outDir = outDirFor("node-fenced"); const result = await compile(join(nodeTypesDir, "fenced.ts"), { From f2b8162a6de0e1bc3fd625c946e3d73c41c96da7 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 09:47:08 -0700 Subject: [PATCH 2/4] Keep the coverage fixture's blocker refused now that array position patterns compile tests/coverage-fixtures/mixed.ts used `const { length: firstItem } = items` as its SC1031 example. Object patterns of positions and length over arrays compile now, so coverage counted 13/13 static. The example now destructures a union source (array | string), which keeps the SC1031 object-pattern fence at the same line and column; only the message's source kind changes, from array-typed to union-typed. Co-Authored-By: Claude Opus 5.5 --- tests/coverage-fixtures/mixed.ts | 2 +- tests/harness/__snapshots__/coverage-mixed.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/coverage-fixtures/mixed.ts b/tests/coverage-fixtures/mixed.ts index 73a36ce47..1ae9db26d 100644 --- a/tests/coverage-fixtures/mixed.ts +++ b/tests/coverage-fixtures/mixed.ts @@ -10,7 +10,7 @@ const handler = (x: number) => x * 2; const anyone: unknown = score(1); const flags = anyone == score(2); // unknown/object coercion keeps the fence; static primitive pairs lower -const { length: firstItem } = items; // object patterns over arrays keep the fence (patterns and defaults compile) +const { length: firstItem } = score(3) > 99 ? items : "abc"; // a union source keeps the object-pattern fence (narrow first) let label = "score: "; label += score(10); console.log(label); diff --git a/tests/harness/__snapshots__/coverage-mixed.txt b/tests/harness/__snapshots__/coverage-mixed.txt index c3b348f9c..cf78aa9e0 100644 --- a/tests/harness/__snapshots__/coverage-mixed.txt +++ b/tests/harness/__snapshots__/coverage-mixed.txt @@ -5,6 +5,6 @@ scriptc coverage tests/coverage-fixtures/mixed.ts in dynamic tier 1 (operate on values typed only at runtime; --explain-dynamic lists them) blockers: - ×1 SC1031 object destructuring of non-record values (the source is array-typed) + ×1 SC1031 object destructuring of non-record values (the source is union-typed) at tests/coverage-fixtures/mixed.ts:13:7 hint: this construct is valid TypeScript outside the compiled subset — rewrite it with a supported equivalent (https://scriptc.dev/limitations) \ No newline at end of file From cf198952e2f021757c40c7b914b618b4b4867109 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 08:55:16 -0700 Subject: [PATCH 3/4] Compile the real jose root entry: string durations, chained builders, navigator jose 6.2.12 now compiles natively through its root entry (`import { jwtVerify, errors } from "jose"`), from TypeScript and JavaScript, with chained SignJWT builders and string durations: - A discarded `a && b;` / `a || b;` statement whose value form fails on its own result type (jose's secs(): `(!m || m[4] && m[1]) && invalid()`) lowers as `if (a) b;` / `if (!a) b;`. The value form still lowers first, so statements that compiled before are unchanged. - In JavaScript, a number-keyed read on a `T[] | null | undefined` value outside an optional chain (`m[2]` after a statement ruled `m` non-null) reads the array arm like any element read: a group that did not participate is undefined instead of a trap. - A JavaScript fluent method whose returns all end in `this` (including comma sequences) narrows a used call result back to the receiver's subclass. A receiver lowered at an ancestor of the class a call needs is bridged with a verified downcast instead of an upcast the validator rejected (the "upcast JWTClaimsBuilder does not extend SignJWT" internal error). - `typeof navigator` is "object" and `navigator.userAgent` is "Node.js/24", as in Node 24, for the free name in JavaScript and under @types/node. Both fold to literals; other members refuse by name. - TypeScript object destructuring of an `any` result of a JavaScript function (jose's jwtVerify) reads its members like the member reads that already compiled. Co-Authored-By: Claude Opus 5.5 --- .../src/generated/node-v24-compatibility.json | 18 +- .../generated/node-v24-backlog.json | 51 +- .../generated/node-v24-internal.json | 28 +- internal/compatibility/static-support.json | 2 + .../frontend/lowering/builtins/navigator.ts | 60 ++ .../src/frontend/lowering/lower-classes.ts | 59 +- .../src/frontend/lowering/lower-exprs.ts | 93 ++- .../src/frontend/lowering/lower-stmts.ts | 106 ++- .../corpus/6331-jose-root-entry/main.ts.json | 6 + .../6332-fluent-this-declaration/main.ts.json | 6 + .../main.mjs.json | 6 + ...6334-discarded-logical-statements.mjs.json | 6 + .../6335-discarded-logical-statements.ts.json | 6 + .../fixtures/node-types/navigator.ts.json | 6 + tests/corpus/6295-jose-hs256/main.mjs | 33 + tests/corpus/6331-jose-root-entry/main.ts | 110 +++ .../node_modules/jose/LICENSE.md | 21 + .../node_modules/jose/dist/types/index.d.ts | 55 ++ .../jose/dist/types/jwe/compact/decrypt.d.ts | 24 + .../jose/dist/types/jwe/compact/encrypt.d.ts | 40 + .../dist/types/jwe/flattened/decrypt.d.ts | 24 + .../dist/types/jwe/flattened/encrypt.d.ts | 46 ++ .../jose/dist/types/jwe/general/decrypt.d.ts | 28 + .../jose/dist/types/jwe/general/encrypt.d.ts | 51 ++ .../jose/dist/types/jwk/embedded.d.ts | 10 + .../jose/dist/types/jwk/thumbprint.d.ts | 15 + .../jose/dist/types/jwks/local.d.ts | 17 + .../jose/dist/types/jwks/remote.d.ts | 94 +++ .../jose/dist/types/jws/compact/sign.d.ts | 20 + .../jose/dist/types/jws/compact/verify.d.ts | 25 + .../jose/dist/types/jws/flattened/sign.d.ts | 22 + .../jose/dist/types/jws/flattened/verify.d.ts | 25 + .../jose/dist/types/jws/general/sign.d.ts | 38 + .../jose/dist/types/jws/general/verify.d.ts | 29 + .../jose/dist/types/jwt/decrypt.d.ts | 34 + .../jose/dist/types/jwt/encrypt.d.ts | 57 ++ .../jose/dist/types/jwt/sign.d.ts | 21 + .../jose/dist/types/jwt/unsecured.d.ts | 19 + .../jose/dist/types/jwt/verify.d.ts | 35 + .../jose/dist/types/key/export.d.ts | 19 + .../dist/types/key/generate_key_pair.d.ts | 33 + .../jose/dist/types/key/generate_secret.d.ts | 30 + .../jose/dist/types/key/import.d.ts | 67 ++ .../node_modules/jose/dist/types/types.d.ts | 736 ++++++++++++++++++ .../jose/dist/types/util/base64url.d.ts | 16 + .../jose/dist/types/util/decode_jwt.d.ts | 8 + .../types/util/decode_protected_header.d.ts | 10 + .../jose/dist/types/util/errors.d.ts | 182 +++++ .../node_modules/jose/dist/webapi/index.js | 70 ++ .../jose/dist/webapi/jwe/compact/decrypt.js | 7 + .../jose/dist/webapi/jwe/compact/encrypt.js | 26 + .../jose/dist/webapi/jwe/flattened/decrypt.js | 15 + .../jose/dist/webapi/jwe/flattened/encrypt.js | 35 + .../jose/dist/webapi/jwe/general/decrypt.js | 40 + .../jose/dist/webapi/jwe/general/encrypt.js | 105 +++ .../jose/dist/webapi/jwk/embedded.js | 30 + .../jose/dist/webapi/jwk/thumbprint.js | 55 ++ .../jose/dist/webapi/jwks/local.js | 59 ++ .../jose/dist/webapi/jwks/remote.js | 101 +++ .../jose/dist/webapi/jws/compact/sign.js | 20 + .../jose/dist/webapi/jws/compact/verify.js | 8 + .../jose/dist/webapi/jws/flattened/sign.js | 23 + .../jose/dist/webapi/jws/flattened/verify.js | 12 + .../jose/dist/webapi/jws/general/sign.js | 58 ++ .../jose/dist/webapi/jws/general/verify.js | 48 ++ .../jose/dist/webapi/jwt/decrypt.js | 13 + .../jose/dist/webapi/jwt/encrypt.js | 45 ++ .../node_modules/jose/dist/webapi/jwt/sign.js | 19 + .../jose/dist/webapi/jwt/unsecured.js | 34 + .../jose/dist/webapi/jwt/verify.js | 13 + .../jose/dist/webapi/key/export.js | 35 + .../jose/dist/webapi/key/generate_key_pair.js | 35 + .../jose/dist/webapi/key/generate_secret.js | 36 + .../jose/dist/webapi/key/import.js | 53 ++ .../node_modules/jose/dist/webapi/lib/asn1.js | 138 ++++ .../jose/dist/webapi/lib/buffer_utils.js | 75 ++ .../dist/webapi/lib/content_encryption.js | 90 +++ .../jose/dist/webapi/lib/deflate.js | 40 + .../jose/dist/webapi/lib/jwe_algorithms.js | 116 +++ .../jose/dist/webapi/lib/jwe_decrypt.js | 187 +++++ .../jose/dist/webapi/lib/jwe_encrypt.js | 75 ++ .../jose/dist/webapi/lib/jws_algorithms.js | 74 ++ .../jose/dist/webapi/lib/jws_sign.js | 46 ++ .../jose/dist/webapi/lib/jws_verify.js | 86 ++ .../jose/dist/webapi/lib/jwt_claims_set.js | 146 ++++ .../node_modules/jose/dist/webapi/lib/key.js | 155 ++++ .../jose/dist/webapi/lib/key_algorithm.js | 15 + .../jose/dist/webapi/lib/key_descriptor.js | 9 + .../jose/dist/webapi/lib/key_management.js | 196 +++++ .../jose/dist/webapi/lib/validate.js | 122 +++ .../jose/dist/webapi/util/base64url.js | 16 + .../jose/dist/webapi/util/decode_jwt.js | 33 + .../webapi/util/decode_protected_header.js | 19 + .../jose/dist/webapi/util/errors.js | 109 +++ .../node_modules/jose/package.json | 134 ++++ .../6332-fluent-this-declaration/main.ts | 17 + .../node_modules/fluent/index.d.ts | 14 + .../node_modules/fluent/index.js | 31 + .../node_modules/fluent/package.json | 11 + .../6333-fluent-this-declaration-js/main.mjs | 17 + .../node_modules/fluent/index.d.ts | 14 + .../node_modules/fluent/index.js | 31 + .../node_modules/fluent/package.json | 11 + .../6334-discarded-logical-statements.mjs | 42 + .../6335-discarded-logical-statements.ts | 27 + tests/fixtures/node-types/fenced.ts | 4 + tests/fixtures/node-types/navigator.ts | 6 + .../__snapshots__/node-types-fenced.txt | 11 +- .../6331-jose-root-entry/main.ts.json | 1 + .../6332-fluent-this-declaration/main.ts.json | 1 + .../main.mjs.json | 1 + ...6334-discarded-logical-statements.mjs.json | 1 + .../6335-discarded-logical-statements.ts.json | 1 + tests/harness/project-config.test.ts | 18 + 114 files changed, 5525 insertions(+), 57 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/builtins/navigator.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json create mode 100644 tests/corpus/6331-jose-root-entry/main.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js create mode 100644 tests/corpus/6331-jose-root-entry/node_modules/jose/package.json create mode 100644 tests/corpus/6332-fluent-this-declaration/main.ts create mode 100644 tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts create mode 100644 tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js create mode 100644 tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json create mode 100644 tests/corpus/6333-fluent-this-declaration-js/main.mjs create mode 100644 tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts create mode 100644 tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js create mode 100644 tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json create mode 100644 tests/corpus/6334-discarded-logical-statements.mjs create mode 100644 tests/corpus/6335-discarded-logical-statements.ts create mode 100644 tests/fixtures/node-types/navigator.ts create mode 100644 tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json create mode 100644 tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json create mode 100644 tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json create mode 100644 tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json create mode 100644 tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index de189f9fd..e2cd6fdfe 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -328,10 +328,10 @@ "entries": 92, "apiEntries": 92, "static": { - "supported": 0, - "partial": 14, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 77, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -37165,9 +37165,9 @@ "inherited": false }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -37303,9 +37303,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "supported", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index b9657ac21..83cb57ca8 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -3,12 +3,12 @@ "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", "summary": { - "taskCount": 3456, - "tierItemCount": 3456, + "taskCount": 3455, + "tierItemCount": 3455, "tiers": { "static": { - "audit-partial": 1176, - "verify-gap": 2152, + "audit-partial": 1177, + "verify-gap": 2150, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -17,7 +17,7 @@ "priorities": { "high": 2289, "normal": 694, - "low": 473 + "low": 472 } }, "tasks": [ @@ -34884,12 +34884,16 @@ "priority": "low", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] } } }, @@ -35018,31 +35022,6 @@ } } }, - { - "id": "globals:fc0a32e8c26f", - "chapter": "globals", - "label": "userAgent", - "signature": "`userAgent` Type: {string}", - "apiSymbol": "userAgent", - "docsUrl": "https://nodejs.org/docs/v24.22.0/api/globals.html#navigator", - "nodeStability": { - "index": "1.1", - "level": 1, - "text": "Active development. Disable this API with the [`--no-experimental-global-navigator`][] CLI flag.", - "inherited": true - }, - "priority": "low", - "tiers": { - "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:globals", - "tests": [] - } - } - }, { "id": "globals:b412bf9a456d", "chapter": "globals", diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 333926a36..254e754c2 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -340,10 +340,10 @@ "entries": 94, "apiEntries": 92, "static": { - "supported": 0, - "partial": 14, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 77, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52231,8 +52231,11 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/6331-jose-root-entry/main.ts", "tests/corpus/text-codec-values/main.ts", - "tests/harness/fetch-conformance.test.ts" + "tests/fixtures/node-types/navigator.ts", + "tests/harness/fetch-conformance.test.ts", + "tests/harness/project-config.test.ts" ] }, "anchorSource": "chapter" @@ -54247,8 +54250,13 @@ "inherited": false }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "partial", + "evidence": "compiler-feature:globals.navigator", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts", + "tests/corpus/6331-jose-root-entry/main.ts" + ] }, "anchorSource": "exact" }, @@ -54385,8 +54393,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:globals" + "status": "supported", + "evidence": "compiler-feature:globals.userAgent", + "tests": [ + "tests/fixtures/node-types/navigator.ts", + "tests/harness/project-config.test.ts" + ] }, "anchorSource": "ancestor" }, diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 3d81a37f7..3b0bfe31e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -187,6 +187,8 @@ { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, + { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, { "chapter": "errors", "parent": "Class: `SystemError`", "symbols": ["code", "errno", "syscall", "path", "dest"], "status": "partial", "evidence": ["tests/corpus/caught-system-error-fields.ts", "tests/fixtures/node-types/terminal-error-fields.ts", "tests/harness/project-config.test.ts"] }, {"chapter": "events", "symbols": ["EventTarget", "eventTarget.addEventListener", "eventTarget.removeEventListener", "eventTarget.dispatchEvent"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5131-event-target-listener-errors.js", "tests/corpus/5132-event-target-typed.ts", "tests/corpus/5133-abort-signal-event-target.ts", "tests/corpus/5135-event-target-warnings.js", "tests/corpus/5240-event-target-subclass.ts", "tests/corpus/5242-event-target-subclass-hierarchy.ts", "tests/corpus/5243-event-target-subclass-dynamic.ts", "tests/corpus/5244-event-target-subclass.js", "tests/corpus/5245-event-target-subclass-signals.ts", "tests/corpus/5248-event-target-subclass-warnings.ts"]}, diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts new file mode 100644 index 000000000..637edaf3c --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts @@ -0,0 +1,60 @@ +/* Node 24's `navigator` global (lib/internal/navigator.js), the part + * libraries read to tell runtimes apart: `typeof navigator` is "object" + * and `navigator.userAgent` is `Node.js/`. jose reads exactly + * that at module load (jwks/remote.js). Both fold to string literals, so + * nothing links. The other members (hardwareConcurrency, language, + * languages, platform, locks) and the object as a value keep a named + * refusal. */ +import * as ts from "../../ts7/adapter.js"; +import { strLit } from "../../../ir/build.js"; +import type { IrExpr } from "../../../ir/ir.js"; +import { isJsSourceFile, locOf } from "../../program.js"; +import type { Lowerer } from "../lowerer.js"; + +/** True when `node` names Node's global `navigator`: the @types/node + * declaration, or, in a JavaScript file, the free name no declaration + * binds (Node defines it on the global object). */ +export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false; + if (lowerer.isStdlibGlobal(expr, "navigator")) return true; + return ( + isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined + ); +} + +/** `navigator.userAgent` (`Node.js/` of `nodeVersion`, the pinned + * release builtins/process.ts names), or the named refusal for another + * member. Null when the receiver is not the global. */ +export function lowerNavigatorMember( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, + nodeVersion: string, +): IrExpr | null { + if (!isNavigatorGlobal(lowerer, access.expression)) return null; + if (access.name.text === "userAgent") { + return strLit(`Node.js/${nodeVersion.split(".")[0]}`, locOf(access)); + } + return refuseNavigator( + lowerer, + access, + `navigator.${access.name.text}`, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** The refusal for the `navigator` object itself or an unlowered member. */ +export function refuseNavigator( + lowerer: Lowerer, + node: ts.Node, + what: string, + symbol?: ts.Symbol, +): never { + return lowerer.noLowering( + what, + node, + "of Node's navigator global only typeof navigator and navigator.userAgent compile", + symbol ?? null, + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 770c74840..668304454 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -178,6 +178,39 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean { return valid; } +/** returnsOnlyThis, also accepting a comma expression whose last operand + * is `this` (the minifier's `return a(), this.x = v, this`, jose's fluent + * claim setters). Used only to narrow a call's result back to the + * receiver's class. */ +export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { + const body = member.body; + const last = body?.statements?.nodes.at(-1); + if (!body || !last || !ts.isReturnStatement(last)) return false; + let valid = true; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node)) return; + if (ts.isReturnStatement(node)) { + let value = node.expression; + for (;;) { + while (value && ts.isParenthesizedExpression(value)) value = value.expression; + if ( + value && + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + value = value.right; + continue; + } + break; + } + if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false; + } + node.forEachChild(visit); + }; + body.forEachChild(visit); + return valid; +} + function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; @@ -6988,19 +7021,43 @@ export function lowerClassGenericMethodCall( } : upcastTo(lowerer, receiver, declarer.def.name); const args = lowerer.completeArgs(call.arguments.nodes, instance.params, loc, call); - return { + const result: IrExpr = { kind: "call", callee: instance.name, args: [thisArg, ...args], type: instance.returnType, loc, }; + // A fluent JavaScript method (every return ends in `this`, jose's claim + // setters) answers the receiver: the instance returns the declarer's + // class, a used result's checker type is the receiver's subclass. + if ( + result.type.kind === "object" && + !ts.isExpressionStatement(call.parent) && + ts.isMethodDeclaration(info.decl) && + isJsSourceFile(info.decl.getSourceFile()) && + returnsOnlyThisValue(info.decl) + ) { + return lowerer.maybeNarrow(result, call); + } + return result; } /** Wraps a derived-class expression in an upcast when the target base * class differs (a no-op reinterpret at runtime; keeps IR types exact). */ export function upcastTo(lowerer: Lowerer, expr: IrExpr, className: string): IrExpr { if (expr.type.kind === "object" && expr.type.className !== className) { + // A value lowered at an ANCESTOR of the class the site needs (a + // fluent `this` result whose lowering kept the method's declaring + // class, the receiver of the next call in a chain): the checker + // vouches for the subclass, so the bridge is the verified downcast, + // never an upcast the validator rejects (an internal error before). + if ( + !lowerer.isSubclassOf(expr.type.className, className) && + lowerer.isSubclassOf(className, expr.type.className) + ) { + return { kind: "downcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; + } return { kind: "upcast", value: expr, type: { kind: "object", className }, loc: expr.loc }; } return expr; diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index adde0eb3f..45379806f 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6,7 +6,7 @@ import { lowerEventObjectProperty, lowerEventObjectPropertyWrite } from "./lower import { lowerDisposableStackInstanceOf } from "./builtins/disposable-stack.js"; import { lowerEventStaticProperty } from "./builtins/numbers.js"; import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js"; -import { isOptionalProcessStreamProperty } from "./builtins/process.js"; +import { isOptionalProcessStreamProperty, NODE_COMPAT_VERSION } from "./builtins/process.js"; import { lowerWorkerMetadata } from "./builtins/workers.js"; import { esmImportBindingKind } from "./builtins/module-bindings.js"; import { lowerReadlineProperty } from "./builtins/readline.js"; @@ -216,6 +216,7 @@ import { errorPropertyRead, errorPropertyWrite, errorToStringCall } from "./erro import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "./lower-stream.js"; import { unionWideningTags } from "../../ir/analysis.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { isNavigatorGlobal, lowerNavigatorMember, refuseNavigator } from "./builtins/navigator.js"; import { globalSymbolKey } from "./expressions/global-symbols.js"; import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js"; import { @@ -667,6 +668,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { return { kind: "strLit", value: "object", type: STRING, loc }; if (lowerer.isStdlibGlobal(expr.expression, "fetch")) return { kind: "strLit", value: "function", type: STRING, loc }; + if (isNavigatorGlobal(lowerer, expr.expression)) + return { kind: "strLit", value: "object", type: STRING, loc }; const performance = lowerPerfHooksTypeof(lowerer, expr.expression); if (performance) return performance; if (isNodeModuleValue(lowerer, expr.expression)) { @@ -1687,6 +1690,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const ns = lowerNsIdentifierValue(lowerer, expr); if (ns) return ns; } + if (isNavigatorGlobal(lowerer, expr)) { + refuseNavigator( + lowerer, + expr, + "the navigator object as a value", + lowerer.checker.getSymbolAtLocation(expr), + ); + } // Preflight guarantees no unresolved identifiers; anything else here // is a blocked declaration's binding (the SC2004 cascade) or a // binding form we don't model yet. @@ -1950,6 +1961,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + if (!expr.questionDotToken) { + const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); + if (navigator) return navigator; + } // `require("./x").member` over a program module's export table: the // module's %init at this position, then the member read exactly like // a `const m = require("./x")` binding's (cjs-inline-require.ts). @@ -7085,6 +7100,10 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } + if (key.type.kind === "f64") { + const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key); + if (safe) return safe; + } if (key.type.kind === "string" || key.type.kind === "f64") { const lit = ts.isStringLiteral(expr.argumentExpression) ? expr.argumentExpression.text @@ -8046,6 +8065,22 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr } return null; } + // `m[2]` on a JavaScript `RegExpExecArray | null` that a previous + // statement ruled non-null without narrowing it (jose's secs()). + if ( + receiverT?.kind === "union" && + isJsSourceFile(expr.getSourceFile()) && + !expr.questionDotToken && + !hasOptionalChainGuard(expr.expression) + ) { + const arms = lowerer.unions.get(receiverT.unionId)?.arms ?? []; + const present = arms.filter((arm) => !isUnitType(arm)); + if (present.length !== 1 || present[0]!.kind !== "array") return null; + const value = lowerer.lowerExpr(expr.expression); + const index = lowerer.lowerExpr(expr.argumentExpression); + if (index.type.kind !== "f64") return null; + return lowerOptionalArrayIndexRead(lowerer, expr, value, index); + } if (receiverT?.kind !== "array") return null; const arr = lowerer.lowerExpr(expr.expression); if (arr.type.kind !== "array") return null; @@ -15323,6 +15358,62 @@ export function lowerUnionProperty( ); } +/** A number-keyed read in JavaScript on an array that may be undefined or + * null outside an optional chain (`m[1]` on a `RegExpExecArray | null` + * that an earlier statement ruled non-null without narrowing it): the + * array arm is an ordinary JavaScript element read, so a hole, a present + * undefined (a regex group that did not participate) or an invalid index + * answers undefined, as lowerSafeIndexRead does for a plain array. The + * unit arms keep the keyed read's answer. The receiver and key evaluate + * once, in source order. Null when the union is not exactly one array arm + * beside unit arms; optional-chain tails and TypeScript keep the keyed + * read. */ +function lowerOptionalArrayIndexRead( + lowerer: Lowerer, + expr: ts.ElementAccessExpression, + value: IrExpr, + key: IrExpr, +): IrExpr | null { + if ( + !isJsSourceFile(expr.getSourceFile()) || + expr.questionDotToken || + hasOptionalChainGuard(expr.expression) + ) + return null; + if (value.type.kind !== "union") return null; + const unionId = value.type.unionId; + const arms = lowerer.unions.get(unionId)?.arms; + if (!arms) return null; + const present = arms.filter((arm) => !isUnitType(arm)); + const arm = present[0]; + if (present.length !== 1 || arm?.kind !== "array" || arm.elem.kind === "void") return null; + const tag = arms.indexOf(arm); + const loc = locOf(expr); + const stmts: IrStmt[] = []; + const stable = (e: IrExpr, name: string): IrExpr => { + if (e.kind === "varRef" || e.kind === "numLit") return e; + const local = lowerer.declareHiddenLocal(name, e.type); + stmts.push({ kind: "varDecl", localId: local.id, init: e, loc }); + return varRef(local.id, e.type, loc); + }; + if (lowerUnionKeyedRead(lowerer, expr, unionId, value, key, null) === null) return null; + const recv = stable(value, "%indexReceiver"); + const index = stable(key, "%indexKey"); + const keyed = lowerUnionKeyedRead(lowerer, expr, unionId, recv, index, null)!; + const narrowed: IrExpr = { kind: "unionNarrow", unionId, tag, value: recv, type: arm, loc }; + const read = lowerSafeIndexRead(lowerer, narrowed, index, loc); + if (read === null) return null; + const result: IrExpr = { + kind: "ternary", + cond: { kind: "unionIsTag", unionId, tag, value: recv, negated: false, type: BOOL, loc }, + then: lowerer.coerceToExpected(read, keyed.type), + else_: keyed, + type: keyed.type, + loc, + }; + return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: keyed.type, loc }; +} + /** The unionDisc generalization: a keyed read on a union receiver whose * arms answer DIFFERENT (but joinable) types. Each arm contributes its * declared answer — an array element's type (number keys), a declared field's type (literal keys), an diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index cbd893d2e..51284e1c8 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1940,6 +1940,43 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool .some((p) => lowerer.checker.declarationsOf(p).some(declarationBelongs)); } +/** True when an `any` value comes from a JavaScript function's result: + * a call (possibly awaited) whose callee is declared in a JavaScript file, + * or a const bound to one. A TypeScript program importing a JavaScript + * package sees the package's inferred types (`jwtVerify` from jose + * answers `any`), and its object destructuring reads the members of that + * checked-dynamic value as JavaScript does — member reads on the same + * value already compile. Other `any` sources keep the fence. */ +function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean { + let expr = node; + while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression; + if (ts.isCallExpression(expr)) { + const callee = ts.isPropertyAccessExpression(expr.expression) + ? expr.expression.name + : expr.expression; + if (!ts.isIdentifier(callee)) return false; + const symbol = lowerer.resolveValueSymbol(callee); + const declarations = symbol ? lowerer.checker.declarationsOf(symbol) : []; + return ( + declarations.length > 0 && + declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d)) + ); + } + if (ts.isIdentifier(expr) && depth === 0) { + const symbol = lowerer.resolveValueSymbol(expr); + const declaration = symbol ? lowerer.checker.valueDeclarationOf(symbol) : undefined; + return ( + declaration !== undefined && + ts.isVariableDeclaration(declaration) && + ts.isIdentifier(declaration.name) && + declaration.initializer !== undefined && + (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 && + javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1) + ); + } + return false; +} + /** Destructuring declarations, desugared as sugar over indexed/field * reads: the initializer evaluates ONCE into a hidden temp, then each * bound name declares (or assigns its pre-registered module global) from @@ -1984,7 +2021,12 @@ export function lowerDestructuringDecl( lowerer.anyOrigin(decl.initializer) && !isJsSourceFile(decl.getSourceFile()) && init.type.kind !== "record" && - init.type.kind !== "array" + init.type.kind !== "array" && + !( + init.type.kind === "dyn" && + ts.isObjectBindingPattern(decl.name) && + javaScriptAnyOrigin(lowerer, decl.initializer, 0) + ) ) { lowerer.anyOpFence("destructuring", decl); } @@ -7372,6 +7414,15 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm if (droppedPureStdlibStatement(lowerer, expr)) { return { kind: "block", body: [], loc: locOf(expr) }; } + if ( + stmtNode !== undefined && + ts.isExpressionStatement(stmtNode) && + ts.isBinaryExpression(expr) && + (expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + expr.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) { + return lowerDiscardedLogical(lowerer, expr); + } const value = lowerer.lowerExpr(expr); // A bare unit-literal statement (`undefined;`): a pure no-op in JS, // and the IR has no bare-unit VALUE outside a union wrap — drop it @@ -7380,6 +7431,59 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm return { kind: "exprStmt", expr: value, loc: locOf(expr) }; } +/** A discarded `a && b` / `a || b` statement (the minifier's `if`). The + * value form lowers first, exactly as before. Only when it fails on the + * logical's own result type — the operands have no common representation + * (jose's `(!m || m[4] && m[1]) && invalid()` joins a boolean, a string + * and undefined into the checker's `string`) — does the statement lower + * as the control flow it is: `if (a) b;` / `if (!a) b;`, a condition test + * that needs no result value. Diagnostics outside the statement, and + * internal errors, keep the original failure. */ +function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrStmt { + const diagsBefore = lowerer.diags.length; + try { + const value = lowerer.lowerExpr(expr); + if (value.kind === "unitLit") return { kind: "block", body: [], loc: locOf(expr) }; + return { kind: "exprStmt", expr: value, loc: locOf(expr) }; + } catch (e) { + // A probe (a capture buffer is active) keeps its failure: its owner + // picks another lowering, exactly as before. + if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e; + const added = lowerer.diags.slice(diagsBefore); + const file = expr.getSourceFile().fileName; + const start = expr.getStart(); + const local = + added.length > 0 && + added.every( + (d) => + d.code !== "SC9001" && + d.loc.file === file && + d.loc.start >= start && + d.loc.end <= expr.end, + ); + if (!local) throw e; + lowerer.diags.length = diagsBefore; + } + const and = expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken; + const cond = lowerer.lowerCondition(expr.left); + if (cond.kind === "boolLit") { + return cond.value === and + ? lowerExprStatement(lowerer, expr.right) + : { kind: "block", body: [], loc: locOf(expr) }; + } + const right = (): IrStmt[] => + lowerer.narrowingAliases(aliasTypeofNarrows(lowerer, expr.left, and), () => + and + ? withRuntimeOptionalNarrowed(lowerer, runtimeOptionalTrueIds(lowerer, expr.left), () => [ + lowerExprStatement(lowerer, expr.right), + ]) + : [lowerExprStatement(lowerer, expr.right)], + ); + return and + ? { kind: "if", cond, then: right(), else_: [], loc: locOf(expr) } + : { kind: "if", cond, then: [], else_: right(), loc: locOf(expr) }; +} + /** True for a statement-position expression whose evaluation cannot be * observed once the value is discarded: * - a property/element READ (`?.` included) whose receiver IS a diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..ad7513645 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6331-jose-root-entry/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6331-jose-root-entry/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..d848dd493 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6332-fluent-this-declaration/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..4bde02942 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6333-fluent-this-declaration-js/main.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..e510d9e2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6334-discarded-logical-statements.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6334-discarded-logical-statements.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..fd7b159a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6335-discarded-logical-statements.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6335-discarded-logical-statements.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json new file mode 100644 index 000000000..27fe582a8 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/navigator.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/navigator.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6295-jose-hs256/main.mjs b/tests/corpus/6295-jose-hs256/main.mjs index 253fa66e4..a8a669a57 100644 --- a/tests/corpus/6295-jose-hs256/main.mjs +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -68,3 +68,36 @@ const cryptoKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", h await verify("crypto key", token, {}, cryptoKey); const signingKey = await crypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-512" }, false, ["sign"]); await verify("wrong key hash", token, {}, signingKey); + +// Chained builder calls: jose's declarations type every setter's result as +// `this`, implemented by the JWTClaimsBuilder base SignJWT extends. +const chained = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience(["urn:scriptc:audience", "urn:other"]) + .setSubject("bob") + .setJti("jti-2") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 60) + .sign(secret); +console.log(chained); +await verify("chained", chained, { audience: "urn:scriptc:audience" }); + +// String durations: jose's secs() parses them. +for (const maxTokenAge of ["10 minutes", "1 hour", "1.5h", "2 days", "+20m", "1 week", "1y", "900 secs", "3 HOURS", "2h ago", "-2h ago", "10 fortnights", ""]) { + await verify(`max token age ${JSON.stringify(maxTokenAge)}`, token, { maxTokenAge }); +} +for (const clockTolerance of ["30s", "5 sec", "1 min from now", "1 min ago"]) { + await verify(`clock tolerance ${clockTolerance}`, token, { currentDate: new Date((iat + 3620) * 1000), clockTolerance }); +} +const relative = new SignJWT({ role: "reader" }); +relative.setProtectedHeader({ alg: "HS256" }).setIssuedAt("-1 minute").setExpirationTime("2h"); +const claims = JSON.parse(Buffer.from((await relative.sign(secret)).split(".")[1], "base64url").toString()); +const clock = Math.floor(Date.now() / 1000); +console.log(Math.abs(claims.exp - clock - 7200) <= 2, Math.abs(claims.iat - clock + 60) <= 2); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (e) { + console.log(e.name, e.message); +} diff --git a/tests/corpus/6331-jose-root-entry/main.ts b/tests/corpus/6331-jose-root-entry/main.ts new file mode 100644 index 000000000..598154040 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/main.ts @@ -0,0 +1,110 @@ +// The real jose@6.2.12 through its root entry, from TypeScript, the way a +// Next.js proxy uses it: `import { jwtVerify, errors } from "jose"`. +// node_modules/jose vendors the published package's dist/ (every webapi +// module the root entry loads, and its declarations), package.json and +// LICENSE.md unmodified (MIT). Loading the root runs jwks/remote.js, which +// reads Node's navigator global at module load. Covers chained SignJWT +// builders, string durations (jose's secs()), destructured jwtVerify +// results and `instanceof errors.JOSEError` brand checks. +import { SignJWT, jwtVerify, errors, cryptoRuntime, decodeJwt } from "jose"; + +const encoder = new TextEncoder(); +const secret = encoder.encode("scriptc-jose-root-shared-secret"); +const now = 1_800_000_000; +const iat = 1_799_999_000; + +interface Options { + issuer?: string; + audience?: string; + algorithms?: string[]; + at?: number; + maxTokenAge?: string; + clockTolerance?: string; +} + +async function check(label: string, token: string, options: Options = {}): Promise { + try { + const { payload, protectedHeader } = await jwtVerify(token, secret, { + algorithms: options.algorithms ?? ["HS256"], + currentDate: new Date((options.at ?? now) * 1000), + issuer: options.issuer, + audience: options.audience, + maxTokenAge: options.maxTokenAge, + clockTolerance: options.clockTolerance, + }); + const role = payload.role; + console.log(label, "ok", protectedHeader.alg, payload.sub ?? null, role, payload.exp ?? null); + } catch (error) { + if (error instanceof errors.JOSEError) { + console.log(label, error.code, error.message, error instanceof errors.JWTExpired); + } else if (error instanceof TypeError) { + console.log(label, "TypeError", error.message); + } else { + throw error; + } + } +} + +console.log(cryptoRuntime, errors.JWTExpired.code, errors.JWSSignatureVerificationFailed.code); + +const token = await new SignJWT({ role: "admin" }) + .setProtectedHeader({ alg: "HS256", typ: "JWT" }) + .setIssuedAt(iat) + .setIssuer("urn:scriptc:issuer") + .setAudience("urn:scriptc:audience") + .setSubject("alice") + .setJti("jti-1") + .setNotBefore(iat - 60) + .setExpirationTime(iat + 3600) + .sign(secret); +console.log(token); +console.log(JSON.stringify(decodeJwt(token))); + +await check("valid", token, { issuer: "urn:scriptc:issuer", audience: "urn:scriptc:audience" }); +await check("issuer", token, { issuer: "urn:other" }); +await check("audience", token, { audience: "urn:other" }); +await check("expired", token, { at: iat + 7200 }); +await check("wrong secret", await new SignJWT({ role: "admin" }).setProtectedHeader({ alg: "HS256" }).sign(encoder.encode("x"))); +await check("malformed", "not.a.jwt"); +await check("alg", token, { algorithms: ["HS512"] }); + +// String durations: maxTokenAge and clockTolerance against the fixed date. +await check("max age 10 minutes", token, { maxTokenAge: "10 minutes" }); +await check("max age 1 hour", token, { maxTokenAge: "1 hour" }); +await check("max age 2 days", token, { maxTokenAge: "2 days" }); +await check("max age 1.5h", token, { maxTokenAge: "1.5h" }); +await check("max age +20m", token, { maxTokenAge: "+20m" }); +await check("max age 1 week", token, { maxTokenAge: "1 week" }); +await check("max age 1y", token, { maxTokenAge: "1y" }); +await check("max age 900 secs", token, { maxTokenAge: "900 secs" }); +await check("max age 3 HOURS", token, { maxTokenAge: "3 HOURS" }); +await check("tolerance 30s", token, { at: iat + 3620, clockTolerance: "30s" }); +await check("tolerance 5 sec", token, { at: iat + 3620, clockTolerance: "5 sec" }); +await check("tolerance 1 min from now", token, { at: iat + 3620, clockTolerance: "1 min from now" }); +await check("tolerance 1 min ago", token, { at: iat + 3620, clockTolerance: "1 min ago" }); +await check("bad duration", token, { maxTokenAge: "10 fortnights" }); +await check("signed ago", token, { maxTokenAge: "-2h ago" }); +await check("empty duration", token, { maxTokenAge: "" }); + +// String durations on the builder are relative to the clock: check the +// distance from now instead of printing it. +const relative = await new SignJWT({ role: "reader" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt("-1 minute") + .setNotBefore("0s") + .setExpirationTime("2h") + .sign(secret); +const claims = decodeJwt(relative); +const clock = Math.floor(Date.now() / 1000); +console.log( + Math.abs((claims.exp ?? 0) - clock - 7200) <= 2, + Math.abs((claims.iat ?? 0) - clock + 60) <= 2, + Math.abs((claims.nbf ?? 0) - clock) <= 2, +); +const { payload } = await jwtVerify(relative, secret, { maxTokenAge: "5 minutes" }); +console.log(payload.role); +try { + new SignJWT({}).setExpirationTime("soon"); +} catch (error) { + console.log((error as Error).name, (error as Error).message); +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md new file mode 100644 index 000000000..d0ec038a8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Filip Skokan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts new file mode 100644 index 000000000..730247dd1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/index.d.ts @@ -0,0 +1,55 @@ +export { compactDecrypt } from './jwe/compact/decrypt.js'; +export type { CompactDecryptGetKey } from './jwe/compact/decrypt.js'; +export { flattenedDecrypt } from './jwe/flattened/decrypt.js'; +export type { FlattenedDecryptGetKey } from './jwe/flattened/decrypt.js'; +export { generalDecrypt } from './jwe/general/decrypt.js'; +export type { GeneralDecryptGetKey } from './jwe/general/decrypt.js'; +export { GeneralEncrypt } from './jwe/general/encrypt.js'; +export type { Recipient } from './jwe/general/encrypt.js'; +export { compactVerify } from './jws/compact/verify.js'; +export type { CompactVerifyGetKey } from './jws/compact/verify.js'; +export { flattenedVerify } from './jws/flattened/verify.js'; +export type { FlattenedVerifyGetKey } from './jws/flattened/verify.js'; +export { generalVerify } from './jws/general/verify.js'; +export type { GeneralVerifyGetKey } from './jws/general/verify.js'; +export { jwtVerify } from './jwt/verify.js'; +export type { JWTVerifyOptions, JWTVerifyGetKey } from './jwt/verify.js'; +export { jwtDecrypt } from './jwt/decrypt.js'; +export type { JWTDecryptOptions, JWTDecryptGetKey } from './jwt/decrypt.js'; +export { CompactEncrypt } from './jwe/compact/encrypt.js'; +export { FlattenedEncrypt } from './jwe/flattened/encrypt.js'; +export { CompactSign } from './jws/compact/sign.js'; +export { FlattenedSign } from './jws/flattened/sign.js'; +export { GeneralSign } from './jws/general/sign.js'; +export type { Signature } from './jws/general/sign.js'; +export { SignJWT } from './jwt/sign.js'; +export { EncryptJWT } from './jwt/encrypt.js'; +export { calculateJwkThumbprint, calculateJwkThumbprintUri } from './jwk/thumbprint.js'; +export { EmbeddedJWK } from './jwk/embedded.js'; +export { createLocalJWKSet } from './jwks/local.js'; +export type { LocalJWKSet } from './jwks/local.js'; +export { createRemoteJWKSet, jwksCache, customFetch } from './jwks/remote.js'; +export type { RemoteJWKSet, RemoteJWKSetOptions, JWKSCacheInput, ExportedJWKSCache, FetchImplementation, } from './jwks/remote.js'; +export { UnsecuredJWT } from './jwt/unsecured.js'; +export type { UnsecuredResult } from './jwt/unsecured.js'; +export { exportPKCS8, exportSPKI, exportJWK } from './key/export.js'; +export { importSPKI, importPKCS8, importX509, importJWK } from './key/import.js'; +export type { KeyImportOptions } from './key/import.js'; +export { decodeProtectedHeader } from './util/decode_protected_header.js'; +export { decodeJwt } from './util/decode_jwt.js'; +export type { ProtectedHeaderParameters } from './util/decode_protected_header.js'; +import * as errors from './util/errors.js'; +export { errors }; +export { generateKeyPair } from './key/generate_key_pair.js'; +export type { GenerateKeyPairAlgorithm, GenerateKeyPairResult, GenerateKeyPairOptions, } from './key/generate_key_pair.js'; +export { generateSecret } from './key/generate_secret.js'; +export type { GenerateSecretAlgorithm, GenerateSecretOptions } from './key/generate_secret.js'; +import * as base64url from './util/base64url.js'; +export { base64url }; +export type { AnyJWK, CompactDecryptResult, CompactJWEHeaderParameters, CompactJWSHeaderParameters, CompactVerifyResult, CritOption, CryptoKey, DecryptOptions, EncryptOptions, FlattenedDecryptResult, FlattenedJWE, FlattenedJWS, FlattenedJWSInput, FlattenedVerifyResult, GeneralDecryptResult, GeneralJWE, GeneralJWS, GeneralJWSInput, GeneralVerifyResult, GetKeyFunction, JoseHeaderParameters, JSONWebKeySet, JWEContentEncryptionAlgorithm, JWEHeaderParameters, JWEKeyManagementAlgorithm, JWEKeyManagementHeaderParameters, JWK_AKP_Private, JWK_AKP_Public, JWK_EC_Private, JWK_EC_Public, JWK_oct, JWK_OKP_Private, JWK_OKP_Public, JWK_RSA_Private, JWK_RSA_Public, JWK, JWKKeyType, JWKParameters, JWSAlgorithm, JWSHeaderParameters, JWTClaimVerificationOptions, JWTDecryptResult, JWTHeaderParameters, JWTPayload, JWTVerifyResult, KeyInput, KeyObject, ProduceJWT, ResolvedKey, SignOptions, VerifyOptions, } from './types.d.ts'; +/** + * Identified the loaded runtime-specific build in prior releases; now always `"WebCryptoAPI"`. + * + * @deprecated Remove any runtime branching on this value; it is always `"WebCryptoAPI"`. + */ +export declare const cryptoRuntime = "WebCryptoAPI"; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts new file mode 100644 index 000000000..abbac7ac2 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWE decryption from unverified headers and token data. */ +export interface CompactDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Compact JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, getKey: CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Compact JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function compactDecrypt(jwe: string | Uint8Array, key: t.KeyInput | CompactDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts new file mode 100644 index 000000000..966f65c3f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts @@ -0,0 +1,40 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Compact JWE strings. */ +export declare class CompactEncrypt { + #private; + /** + * Creates a Compact JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Encrypts the plaintext as a Compact JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts new file mode 100644 index 000000000..b166f8f40 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts @@ -0,0 +1,24 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWE decryption from unverified headers and token data. */ +export interface FlattenedDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Flattened JWE. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a Flattened JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, getKey: FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a Flattened JWE with a key or key resolver. The result includes `key` only when a + * resolver is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function flattenedDecrypt(jwe: t.FlattenedJWE, key: t.KeyInput | FlattenedDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts new file mode 100644 index 000000000..9d1d3e3fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts @@ -0,0 +1,46 @@ +import type * as t from '../../types.d.ts'; +/** Builds and encrypts Flattened JWE objects. */ +export declare class FlattenedEncrypt { + #private; + /** + * Creates a Flattened JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** + * Sets a content encryption key instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the content encryption IV instead of generating a random one for the JWE "enc" algorithm. + * May only be called once. + * + * @deprecated Use only for testing and vector validation. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Encrypts the plaintext as a Flattened JWE. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts new file mode 100644 index 000000000..67a396c16 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/decrypt.d.ts @@ -0,0 +1,28 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWE decryption from unverified headers and token data. */ +export interface GeneralDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a General JWE. + * + * > Note: Returns plaintext and headers from the first recipient that decrypts successfully. Other + * > recipients may be inspected to enforce serialization rules, but their headers are not included in + * > the result. Rely only on the returned data. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput, options?: t.DecryptOptions): Promise; +/** + * Decrypts a General JWE with a dynamically resolved key, included in the result. + * + * @param getKey Resolves a private key or shared secret from unverified token data. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, getKey: GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; +/** + * Decrypts a General JWE with a key or key resolver. The result includes `key` only when a resolver + * is used. + * + * @param key Private key or shared secret, or a function resolving one. + */ +export declare function generalDecrypt(jwe: t.GeneralJWE, key: t.KeyInput | GeneralDecryptGetKey, options?: t.DecryptOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts new file mode 100644 index 000000000..e738a3a14 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwe/general/encrypt.d.ts @@ -0,0 +1,51 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual recipient in a General JWE. */ +export interface Recipient { + /** Sets the JWE Per-Recipient Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWEHeaderParameters): Recipient; + /** + * Sets key management inputs such as ECDH-ES "apu"/"apv" or PBES2 "p2c". Use this method instead + * of header setters; the resulting parameters are added to the JOSE header. May only be called + * once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): Recipient; + /** + * Adds another recipient to the enclosing {@link GeneralEncrypt} and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** + * Encrypts for all recipients on the enclosing {@link GeneralEncrypt}, using their configured + * keys. + */ + encrypt(): Promise; + /** Returns the enclosing {@link GeneralEncrypt} instance. */ + done(): GeneralEncrypt; +} +/** Builds and encrypts General JWE objects. */ +export declare class GeneralEncrypt { + #private; + /** + * Creates a General JWE encryptor. + * + * @param plaintext Binary representation of the plaintext to encrypt. + */ + constructor(plaintext: Uint8Array); + /** + * Adds a recipient and returns its configuration. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + addRecipient(key: t.KeyInput, options?: t.CritOption): Recipient; + /** Sets the JWE Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWEHeaderParameters): this; + /** Sets the JWE Shared Unprotected Header. May only be called once. */ + setSharedUnprotectedHeader(sharedUnprotectedHeader: t.JWEHeaderParameters): this; + /** Sets additional data to authenticate without encrypting it. */ + setAdditionalAuthenticatedData(aad: Uint8Array): this; + /** Encrypts the plaintext as a General JWE. */ + encrypt(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts new file mode 100644 index 000000000..d02ffcd49 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/embedded.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** + * Resolves a verification key from an embedded "jwk" (JSON Web Key) Header Parameter. This key + * resolver opts JWS and JWT verification into trusting a public key supplied by the token. Use the + * verification function's `algorithms` option to restrict accepted algorithms. + * + * @param token The consumed JWS token. + * @returns The public key from the JWS "jwk" (JSON Web Key) Header Parameter. + */ +export declare function EmbeddedJWK(protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts new file mode 100644 index 000000000..674d7cbe6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwk/thumbprint.d.ts @@ -0,0 +1,15 @@ +import type * as t from '../types.d.ts'; +/** + * Calculates a base64url-encoded JWK Thumbprint. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprint(key: t.JWK | t.CryptoKey | t.KeyObject, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; +/** + * Calculates a JWK Thumbprint URI. CryptoKey inputs must be extractable. + * + * @param key Key to calculate the thumbprint for. + * @param digestAlgorithm Digest algorithm. Defaults to "sha256". + */ +export declare function calculateJwkThumbprintUri(key: t.CryptoKey | t.KeyObject | t.JWK, digestAlgorithm?: 'sha256' | 'sha384' | 'sha512'): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts new file mode 100644 index 000000000..57057c105 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/local.d.ts @@ -0,0 +1,17 @@ +import type * as t from '../types.d.ts'; +/** A key resolver created by {@link createLocalJWKSet}. */ +export interface LocalJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Returns a structured clone of the original JSON Web Key Set. */ + jwks: () => t.JSONWebKeySet; +} +/** + * Creates a resolver for a locally available JSON Web Key Set. Selection uses the header's "alg" + * (Algorithm) and "kid" (Key ID), and respects the JWK's "use" (Public Key Use) and "key_ops" (Key + * Operations). Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param jwks JSON Web Key Set formatted object. + */ +export declare function createLocalJWKSet(jwks: t.JSONWebKeySet): LocalJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts new file mode 100644 index 000000000..0b7055e84 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwks/remote.d.ts @@ -0,0 +1,94 @@ +import type * as t from '../types.d.ts'; +/** + * Configures a custom fetch implementation for remote JWKS retrieval. + * + * > Note: Fetch-like libraries may have incompatible TypeScript signatures even when they accept the + * > supplied arguments at runtime. + */ +export declare const customFetch: unique symbol; +/** Custom fetch function. Must return HTTP 200 with a JSON JWKS. See {@link customFetch}. */ +export type FetchImplementation = ( +/** JWKS URL, passed as the fetch resource. */ +url: string, +/** Options passed to fetch. */ +options: { + headers: Headers; + method: 'GET'; + /** See {@link !Request.redirect} */ + redirect: 'manual'; + signal: AbortSignal; +}) => Promise; +/** + * Symbol used to configure an externally persisted remote JWKS cache. + * + * > Warning: Only trusted application code must be allowed to write this cache; its keys are trusted for + * > signature verification. + */ +export declare const jwksCache: unique symbol; +/** Remote JWKS resolver options. */ +export interface RemoteJWKSetOptions { + /** + * Timeout (in milliseconds) for the HTTP request. When reached the request will be aborted and + * the verification will fail. Must be a non-negative integer. Default is 5000 (5 seconds). + */ + timeoutDuration?: number; + /** + * Time in milliseconds after a successful fetch before a missing key can trigger another fetch. + * Must not be `NaN`. Defaults to 30000 (30 seconds). + */ + cooldownDuration?: number; + /** + * Maximum age of cached keys in milliseconds. Defaults to 600000 (10 minutes); `Infinity` + * disables expiry. Must not be `NaN`. + */ + cacheMaxAge?: number | typeof Infinity; + /** Headers to be sent with the HTTP request. */ + headers?: Record; + /** See {@link jwksCache}. */ + [jwksCache]?: JWKSCacheInput; + /** See {@link customFetch}. */ + [customFetch]?: FetchImplementation; +} +/** Shape of an externally persisted remote JWKS cache. */ +export interface ExportedJWKSCache { + /** Current cached JSON Web Key Set */ + jwks: t.JSONWebKeySet; + /** Last successful fetch, in milliseconds since Unix epoch. */ + uat: number; +} +/** Values accepted by the {@link jwksCache} option. */ +export type JWKSCacheInput = ExportedJWKSCache | Record; +/** A key resolver created by {@link createRemoteJWKSet}. */ +export interface RemoteJWKSet { + (protectedHeader?: t.JWSHeaderParameters, token?: t.FlattenedJWSInput): Promise; + /** Whether the cooldown window following the last successful fetch is still in effect. */ + readonly coolingDown: boolean; + /** + * Whether the currently cached JSON Web Key Set is within its + * {@link RemoteJWKSetOptions.cacheMaxAge}. + */ + readonly fresh: boolean; + /** Whether a JSON Web Key Set fetch is currently in flight. */ + readonly reloading: boolean; + /** + * Triggers a JSON Web Key Set fetch, bypassing + * {@link RemoteJWKSetOptions.cooldownDuration the cooldown}. + */ + reload: () => Promise; + /** + * Returns a structured clone of the cached JSON Web Key Set, or `undefined` before keys have been + * fetched or seeded via {@link jwksCache}. + */ + jwks: () => t.JSONWebKeySet | undefined; +} +/** + * Creates a resolver for a JSON Web Key Set available at an HTTP(S) URL. Fetches the JSON Web Key + * Set when the cache is missing or stale. An unmatched key triggers another fetch only when + * `cooldownDuration` has elapsed since the last successful fetch. Selection uses the header's "alg" + * and "kid" and respects the JWK's "use" and "key_ops". Exactly one key must match. + * + * > Note: Only public signature verification keys are supported, not public encryption keys. + * + * @param url URL to fetch the JSON Web Key Set from. + */ +export declare function createRemoteJWKSet(url: URL, options?: RemoteJWKSetOptions): RemoteJWKSet; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts new file mode 100644 index 000000000..15d7e062b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/sign.d.ts @@ -0,0 +1,20 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Compact JWS strings. */ +export declare class CompactSign { + #private; + /** + * Creates a Compact JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.CompactJWSHeaderParameters): this; + /** + * Signs the payload as a Compact JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts new file mode 100644 index 000000000..0f21744cb --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/compact/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Compact JWS verification from unverified headers and token data. */ +export interface CompactVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Compact JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function compactVerify(jws: string | Uint8Array, getKey: CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Compact JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function compactVerify(jws: string | Uint8Array, key: t.KeyInput | CompactVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts new file mode 100644 index 000000000..3bc861803 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/sign.d.ts @@ -0,0 +1,22 @@ +import type * as t from '../../types.d.ts'; +/** Builds and signs Flattened JWS objects. */ +export declare class FlattenedSign { + #private; + /** + * Creates a Flattened JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): this; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): this; + /** + * Signs the payload as a Flattened JWS. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts new file mode 100644 index 000000000..151c59b9a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/flattened/verify.d.ts @@ -0,0 +1,25 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for Flattened JWS verification from unverified headers and token data. */ +export interface FlattenedVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Flattened JWS signature and decodes its payload. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a Flattened JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, getKey: FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a Flattened JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function flattenedVerify(jws: t.FlattenedJWSInput, key: t.KeyInput | FlattenedVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts new file mode 100644 index 000000000..ad1cd73dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/sign.d.ts @@ -0,0 +1,38 @@ +import type * as t from '../../types.d.ts'; +/** Configures an individual signature in a General JWS. */ +export interface Signature { + /** Sets the JWS Protected Header. May only be called once. */ + setProtectedHeader(protectedHeader: t.JWSHeaderParameters): Signature; + /** Sets the JWS Unprotected Header. May only be called once. */ + setUnprotectedHeader(unprotectedHeader: t.JWSHeaderParameters): Signature; + /** + * Adds another signature to the enclosing {@link GeneralSign} and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Creates all signatures on the enclosing {@link GeneralSign}, using their configured keys. */ + sign(): Promise; + /** Returns the enclosing {@link GeneralSign} instance. */ + done(): GeneralSign; +} +/** Builds and signs General JWS objects. */ +export declare class GeneralSign { + #private; + /** + * Creates a General JWS signer. + * + * @param payload Binary representation of the payload to sign. + */ + constructor(payload: Uint8Array); + /** + * Adds a signature and returns its configuration. + * + * @param key Private key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + addSignature(key: t.KeyInput, options?: t.SignOptions): Signature; + /** Signs the payload as a General JWS. */ + sign(): Promise; +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts new file mode 100644 index 000000000..620325d00 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jws/general/verify.d.ts @@ -0,0 +1,29 @@ +import type * as t from '../../types.d.ts'; +/** Resolves a key for General JWS verification from unverified headers and token data. */ +export interface GeneralVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a General JWS signature and decodes its payload. + * + * > Note: Returns payload and headers from the first signature that verifies successfully. Other entries' + * > headers may be inspected to reject inconsistent use of the JWS Unencoded Payload Option, but are + * > not included in the result. Rely only on the returned data. + * + * @param key Public key or shared secret. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput, options?: t.VerifyOptions): Promise; +/** + * Verifies a General JWS signature and decodes its payload with a dynamically resolved key, + * included in the result. + * + * @param getKey Resolves a public key or shared secret from unverified token data. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, getKey: GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; +/** + * Verifies a General JWS and decodes its payload using a key or key resolver. The result includes + * `key` only when a resolver is used. + * + * @param key Public key or shared secret, or a function resolving one. + */ +export declare function generalVerify(jws: t.GeneralJWSInput, key: t.KeyInput | GeneralVerifyGetKey, options?: t.VerifyOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts new file mode 100644 index 000000000..cdf6a372a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/decrypt.d.ts @@ -0,0 +1,34 @@ +import type * as t from '../types.d.ts'; +/** JWE decryption and JWT Claims Set validation options. */ +export interface JWTDecryptOptions extends t.DecryptOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT decryption key. No token components have been authenticated when this function is + * called. + */ +export interface JWTDecryptGetKey extends t.GetKeyFunction { +} +/** + * Decrypts a Compact JWE-formatted JWT and validates its Claims Set. + * + * @param key Private key or shared secret to decrypt and verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTDecryptOptions): Promise>; +/** + * Decrypts a JWT and validates its claims, returning the dynamically + * {@link t.ResolvedKey.key resolved key}. + * + * @param getKey Function resolving a private key or shared secret to decrypt and verify the JWT + * with. See {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, getKey: JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Private key or shared secret, or a function resolving one, to decrypt and verify the + * JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ +export declare function jwtDecrypt(jwt: string | Uint8Array, key: t.KeyInput | JWTDecryptGetKey, options?: JWTDecryptOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts new file mode 100644 index 000000000..c39e4c994 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/encrypt.d.ts @@ -0,0 +1,57 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const EncryptJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and encrypts Compact JWE-formatted JSON Web Tokens. */ +export declare class EncryptJWT extends EncryptJWT_base { + #private; + /** + * Sets the JWE Protected Header. May only be called once. + * + * @param protectedHeader JWE Protected Header. Must contain an "alg" (JWE Algorithm) and "enc" + * (JWE Encryption Algorithm) properties. + */ + setProtectedHeader(protectedHeader: t.CompactJWEHeaderParameters): this; + /** + * Sets JWE Key Management parameters, such as ECDH-ES "apu" and "apv" or PBES2 "p2c", and adds + * them to the appropriate JOSE Header. Use this instead of the header setters for algorithm + * inputs. May only be called once. + */ + setKeyManagementParameters(parameters: t.JWEKeyManagementHeaderParameters): this; + /** + * Sets the content encryption key. By default, a suitable random key is generated for the JWE + * "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setContentEncryptionKey(cek: Uint8Array): this; + /** + * Sets the Initialization Vector for content encryption. By default, a suitable random IV is + * generated for the JWE "enc" (Encryption Algorithm). May only be called once. + * + * @deprecated For testing and vector validation only; allow random generation in production. + */ + setInitializationVector(iv: Uint8Array): this; + /** + * Replicates the "iss" (Issuer) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateIssuerAsHeader(): this; + /** + * Replicates the "sub" (Subject) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateSubjectAsHeader(): this; + /** + * Replicates the "aud" (Audience) Claim in the JWE Protected Header, exposing it without + * decryption. + */ + replicateAudienceAsHeader(): this; + /** + * Encrypts and returns the JWT. + * + * @param key Public key or shared secret to encrypt the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jwe-alg Algorithm Key Requirements}. + */ + encrypt(key: t.KeyInput, options?: t.EncryptOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts new file mode 100644 index 000000000..67ce10434 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/sign.d.ts @@ -0,0 +1,21 @@ +import type * as t from '../types.d.ts'; +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const SignJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Builds and signs Compact JWS-formatted JSON Web Tokens. */ +export declare class SignJWT extends SignJWT_base { + #private; + /** + * Sets the JWS Protected Header. May only be called once. + * + * @param protectedHeader JWS Protected Header. Must contain an "alg" (JWS Algorithm) property. + */ + setProtectedHeader(protectedHeader: t.JWTHeaderParameters): this; + /** + * Signs and returns the JWT. + * + * @param key Private key or shared secret to sign the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + */ + sign(key: t.KeyInput, options?: t.SignOptions): Promise; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts new file mode 100644 index 000000000..6c3454eef --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/unsecured.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** Decoded Unsecured JWT. */ +export interface UnsecuredResult { + /** JWT Claims Set. */ + payload: PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); + /** The decoded JOSE Header; always `{ "alg": "none" }` for an Unsecured JWT. */ + header: t.JWSHeaderParameters; +} +/** @param payload Initial JWT Claims Set. Defaults to an empty object. */ +declare const UnsecuredJWT_base: new (payload?: t.JWTPayload) => t.ProduceJWT; +/** Encodes and decodes `{ "alg": "none" }` Unsecured JWTs. */ +export declare class UnsecuredJWT extends UnsecuredJWT_base { + private jwt; + /** Encodes the Unsecured JWT. */ + encode(): string; + /** Decodes an unsecured JWT and validates its claims without authenticating the token. */ + static decode(jwt: string, options?: t.JWTClaimVerificationOptions): UnsecuredResult; +} +export {}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts new file mode 100644 index 000000000..6c8b2f32b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/jwt/verify.d.ts @@ -0,0 +1,35 @@ +import type * as t from '../types.d.ts'; +/** JWS verification and JWT Claims Set validation options. */ +export interface JWTVerifyOptions extends t.VerifyOptions, t.JWTClaimVerificationOptions { +} +/** + * Resolves a JWT verification key. No token components have been authenticated when this function + * is called. + */ +export interface JWTVerifyGetKey extends t.GetKeyFunction { +} +/** + * Verifies a Compact JWS-formatted JWT and validates its Claims Set. + * + * @param key Public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput, options?: JWTVerifyOptions): Promise>; +/** + * Verifies the JWT signature and claims, returning the resolved key. + * + * @param getKey Function resolving a public key or shared secret to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, getKey: JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & t.ResolvedKey>; +/** + * Accepts a key or key resolver. Use this overload when forwarding either form; the result includes + * `key` only when a resolver was used. + * + * @param key Key, or function resolving a key, to verify the JWT with. See + * {@link https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements}. + * @param options JWT Verification and JWT Claims Set validation options. + */ +export declare function jwtVerify(jwt: string | Uint8Array, key: t.KeyInput | JWTVerifyGetKey, options?: JWTVerifyOptions): Promise & Partial>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts new file mode 100644 index 000000000..bbc7ef7d4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/export.d.ts @@ -0,0 +1,19 @@ +import type * as t from '../types.d.ts'; +/** + * Exports a public key to PEM-encoded SPKI. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportSPKI(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a private key to PEM-encoded PKCS#8. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportPKCS8(key: t.CryptoKey | t.KeyObject): Promise; +/** + * Exports a key to JWK. CryptoKey inputs must be extractable. + * + * @param key Key to export. + */ +export declare function exportJWK(key: t.CryptoKey | t.KeyObject | Uint8Array): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts new file mode 100644 index 000000000..ecc53339c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_key_pair.d.ts @@ -0,0 +1,33 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateKeyPair}, subject to runtime support. */ +export type GenerateKeyPairAlgorithm = 'PS256' | 'PS384' | 'PS512' | 'RS256' | 'RS384' | 'RS512' | 'RSA-OAEP' | 'RSA-OAEP-256' | 'RSA-OAEP-384' | 'RSA-OAEP-512' | 'ES256' | 'ES384' | 'ES512' | 'Ed25519' | 'EdDSA' | 'ML-DSA-44' | 'ML-DSA-65' | 'ML-DSA-87' | 'ECDH-ES' | 'ECDH-ES+A128KW' | 'ECDH-ES+A192KW' | 'ECDH-ES+A256KW' | (string & {}); +/** Generated asymmetric key pair. */ +export interface GenerateKeyPairResult { + privateKey: t.CryptoKey; + publicKey: t.CryptoKey; +} +/** Asymmetric key pair generation options. */ +export interface GenerateKeyPairOptions { + /** + * EC curve or OKP key subtype. Must be supported by the algorithm and runtime. ECDH-ES defaults + * to P-256. + */ + crv?: string; + /** RSA modulus length in bits. Must be an integer of at least 2048; defaults to 2048. */ + modulusLength?: number; + /** + * Whether the private key is extractable. Defaults to false; the public key is always + * extractable. + */ + extractable?: boolean; +} +/** + * Generates an asymmetric key pair for a JWA algorithm identifier. + * + * > Note: Private keys are not extractable by default. Set {@link GenerateKeyPairOptions.extractable} to + * > export them; public keys are always extractable. + * + * @param alg JWA Algorithm Identifier to be used with the generated key pair. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateKeyPair(alg: GenerateKeyPairAlgorithm, options?: GenerateKeyPairOptions): Promise; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts new file mode 100644 index 000000000..da1db236e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/generate_secret.d.ts @@ -0,0 +1,30 @@ +import type * as t from '../types.d.ts'; +/** Algorithms supported by {@link generateSecret}, subject to runtime support. */ +export type GenerateSecretAlgorithm = 'HS256' | 'HS384' | 'HS512' | 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' | 'A128KW' | 'A192KW' | 'A256KW' | 'A128GCMKW' | 'A192GCMKW' | 'A256GCMKW' | 'A128GCM' | 'A192GCM' | 'A256GCM' | (string & {}); +/** + * Maps a JWA algorithm identifier to the value returned by {@link generateSecret}. AES-CBC-HMAC + * algorithms return {@link !Uint8Array}; other supported algorithms return + * {@link t.CryptoKey CryptoKey}. When the algorithm is not statically known, the result is their + * union. + */ +export type GeneratedSecret = Alg extends 'A128CBC-HS256' | 'A192CBC-HS384' | 'A256CBC-HS512' ? Uint8Array : string extends Alg ? t.CryptoKey | Uint8Array : t.CryptoKey; +/** Secret generation options. */ +export interface GenerateSecretOptions { + /** + * Whether the generated CryptoKey is extractable. Defaults to false; has no effect for + * A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512, which return raw bytes. + */ + extractable?: boolean; +} +/** + * Generates a symmetric secret key for a given JWA algorithm identifier. + * + * > Note: The secret key is generated with `extractable` set to `false` by default. + * + * > Note: A128CBC-HS256, A192CBC-HS384, and A256CBC-HS512 return {@link !Uint8Array} because these secrets + * > have no CryptoKey representation. + * + * @param alg JWA Algorithm Identifier to be used with the generated secret. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function generateSecret(alg: Alg, options?: GenerateSecretOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts new file mode 100644 index 000000000..88ba06a28 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/key/import.d.ts @@ -0,0 +1,67 @@ +import type * as t from '../types.d.ts'; +/** + * Maps a JWK key type to the value returned by {@link importJWK}. An "oct" JWK returns + * {@link !Uint8Array}; other supported key types return {@link t.CryptoKey CryptoKey}. A JWK + * whose "kty" is not statically known resolves to their union. + */ +export type ImportedJWK = JWKType extends { + kty: 'oct'; +} ? Uint8Array : JWKType extends { + kty: 'AKP' | 'EC' | 'OKP' | 'RSA'; +} ? t.CryptoKey : t.CryptoKey | Uint8Array; +/** Key import options. */ +export interface KeyImportOptions { + /** + * Whether the imported CryptoKey is extractable. Overrides JWK "ext" when set. Without either, + * defaults to false for private keys, true otherwise. + */ + extractable?: boolean; +} +/** + * Imports a PEM-encoded SPKI string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param spki PEM-encoded SPKI string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importSPKI(spki: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded X.509 certificate's public key as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param x509 X.509 certificate string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importX509(x509: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a PEM-encoded PKCS#8 string as a {@link !CryptoKey}. + * + * > Note: For RSA keys, use the rsaEncryption OID (1.2.840.113549.1.1.1). The id-RSASSA-PSS OID + * > (1.2.840.113549.1.1.10) is not supported by the Web Cryptography API. + * + * @param pkcs8 PEM-encoded PKCS#8 string + * @param alg JSON Web Algorithm identifier to be used with the imported key. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importPKCS8(pkcs8: string, alg: string, options?: KeyImportOptions): Promise; +/** + * Imports a JWK as a {@link !CryptoKey} or {@link !Uint8Array}. Asymmetric imports require either the + * "alg" argument or JWK "alg" parameter. For AKP keys, the JWK "alg" parameter is required and must + * match the argument when provided. + * + * > Note: CryptoKey imports honor the JWK "key_ops" and "ext" parameters. + * + * > Note: Symmetric JWKs (`kty: "oct"`) return {@link !Uint8Array} instead of CryptoKey. + * + * @param jwk JSON Web Key. + * @param alg JSON Web Algorithm identifier to be used with the imported key. Default is the "alg" + * property on the JWK. See + * {@link https://github.com/panva/jose/issues/210 Algorithm Key Requirements}. + */ +export declare function importJWK(jwk: JWKType, alg?: string, options?: KeyImportOptions): Promise>; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts new file mode 100644 index 000000000..9815c018d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/types.d.ts @@ -0,0 +1,736 @@ +/** Supported JWS "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWSAlgorithm = + | 'HS256' + | 'HS384' + | 'HS512' + | 'RS256' + | 'RS384' + | 'RS512' + | 'PS256' + | 'PS384' + | 'PS512' + | 'ES256' + | 'ES384' + | 'ES512' + | 'EdDSA' + | 'Ed25519' + | 'ML-DSA-44' + | 'ML-DSA-65' + | 'ML-DSA-87' + | (string & {}) + +/** Supported JWE "alg" (Algorithm) Header Parameter values. Availability depends on the runtime. */ +export type JWEKeyManagementAlgorithm = + | 'dir' + | 'A128KW' + | 'A192KW' + | 'A256KW' + | 'A128GCMKW' + | 'A192GCMKW' + | 'A256GCMKW' + | 'ECDH-ES' + | 'ECDH-ES+A128KW' + | 'ECDH-ES+A192KW' + | 'ECDH-ES+A256KW' + | 'RSA-OAEP' + | 'RSA-OAEP-256' + | 'RSA-OAEP-384' + | 'RSA-OAEP-512' + | 'PBES2-HS256+A128KW' + | 'PBES2-HS384+A192KW' + | 'PBES2-HS512+A256KW' + | (string & {}) + +/** + * Supported JWE "enc" (Encryption Algorithm) Header Parameter values. Availability depends on the + * runtime. + */ +export type JWEContentEncryptionAlgorithm = + | 'A128CBC-HS256' + | 'A192CBC-HS384' + | 'A256CBC-HS512' + | 'A128GCM' + | 'A192GCM' + | 'A256GCM' + | (string & {}) + +/** JWK "kty" (Key Type) Parameter values supported by this module. */ +export type JWKKeyType = 'EC' | 'RSA' | 'OKP' | 'AKP' | 'oct' | (string & {}) + +/** + * Generic JSON Web Key Parameters. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. + */ +export type JWKParameters = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string +} + +/** Convenience interface for public OKP JSON Web Keys. */ +export interface JWK_OKP_Public extends JWKParameters { + /** Key pair subtype. */ + crv: string + /** Public key. */ + x: string +} + +/** Convenience interface for private OKP JSON Web Keys. */ +export interface JWK_OKP_Private extends JWK_OKP_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public AKP JSON Web Keys. */ +export interface JWK_AKP_Public extends JWKParameters { + /** JWK "alg" (Algorithm) Parameter */ + alg: string + + /** AKP JWK "pub" (The Public key) Parameter */ + pub: string +} + +/** Convenience interface for private AKP JSON Web Keys. */ +export interface JWK_AKP_Private extends JWK_AKP_Public { + /** AKP JWK "priv" (The Private Key) Parameter */ + priv: string +} + +/** Convenience interface for public EC JSON Web Keys. */ +export interface JWK_EC_Public extends JWKParameters { + /** Curve. */ + crv: string + /** Public key X coordinate. */ + x: string + /** Public key Y coordinate. */ + y: string +} + +/** Convenience interface for private EC JSON Web Keys. */ +export interface JWK_EC_Private extends JWK_EC_Public { + /** Private key. */ + d: string +} + +/** Convenience interface for public RSA JSON Web Keys. */ +export interface JWK_RSA_Public extends JWKParameters { + /** Public exponent. */ + e: string + /** Modulus. */ + n: string +} + +/** Convenience interface for private RSA JSON Web Keys. */ +export interface JWK_RSA_Private extends JWK_RSA_Public { + /** Private exponent. */ + d: string + /** First factor CRT exponent. */ + dp: string + /** Second factor CRT exponent. */ + dq: string + /** First prime factor. */ + p: string + /** Second prime factor. */ + q: string + /** First CRT coefficient. */ + qi: string +} + +/** Convenience interface for "oct" JSON Web Keys. */ +export interface JWK_oct extends JWKParameters { + /** Symmetric key value. */ + k: string +} + +/** + * JSON Web Key ({@link https://www.rfc-editor.org/info/rfc7517/ JWK}). "RSA", "EC", "OKP", "AKP", + * and "oct" key types are supported. + * + * > Note: This is declared as a type alias rather than an interface so that it satisfies the implicit index + * > signature of the `JsonWebKey` types shipped by `@types/node` and `lib.dom`. It spells out the + * > {@link JWKParameters} members rather than intersecting them so that every JWK member is documented + * > in one place. + */ +export type JWK = { + /** JWK "kty" (Key Type) Parameter */ + kty?: JWKKeyType + /** JWK "alg" (Algorithm) Parameter */ + alg?: JWSAlgorithm | JWEKeyManagementAlgorithm | JWEContentEncryptionAlgorithm + /** Permitted key operations. */ + key_ops?: string[] + /** Whether the key may be exported. */ + ext?: boolean + + /** JWK "use" (Public Key Use) Parameter */ + use?: 'sig' | 'enc' | (string & {}) + /** X.509 certificate chain. */ + x5c?: string[] + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + /** X.509 certificate SHA-256 thumbprint. */ + 'x5t#S256'?: string + /** X.509 certificate URL. */ + x5u?: string + + /** JWK "kid" (Key ID) Parameter */ + kid?: string + /** EC curve or OKP key pair subtype. */ + crv?: string + /** Private RSA exponent, EC key, or OKP key. */ + d?: string + /** RSA first factor CRT exponent. */ + dp?: string + /** RSA second factor CRT exponent. */ + dq?: string + /** RSA public exponent. */ + e?: string + /** Symmetric key value. */ + k?: string + /** RSA modulus. */ + n?: string + /** RSA first prime factor. */ + p?: string + /** RSA second prime factor. */ + q?: string + /** RSA first CRT coefficient. */ + qi?: string + /** EC public key X coordinate or OKP public key. */ + x?: string + /** EC public key Y coordinate. */ + y?: string + + /** AKP JWK "pub" (Public Key) Parameter */ + pub?: string + + /** AKP JWK "priv" (Private key) Parameter */ + priv?: string + /** Additional RSA prime factors. */ + oth?: Array<{ + /** Factor CRT exponent. */ + d?: string + /** Prime factor. */ + r?: string + /** Factor CRT coefficient. */ + t?: string + }> +} + +/** Discriminated union of supported JSON Web Key shapes, narrowed by the "kty" (Key Type) Parameter. */ +// Intersect "kty" into each member separately: typedoc omits the parentheses in `X & (A | B)`. +export type AnyJWK = + | (JWK_EC_Private & { kty: 'EC' }) + | (JWK_EC_Public & { kty: 'EC' }) + | (JWK_RSA_Private & { kty: 'RSA' }) + | (JWK_RSA_Public & { kty: 'RSA' }) + | (JWK_OKP_Private & { kty: 'OKP' }) + | (JWK_OKP_Public & { kty: 'OKP' }) + | (JWK_AKP_Private & { kty: 'AKP' }) + | (JWK_AKP_Public & { kty: 'AKP' }) + | (JWK_oct & { kty: 'oct' }) + +/** Key or secret input accepted by all sign, verify, encrypt, and decrypt operations. */ +export type KeyInput = CryptoKey | KeyObject | JWK | Uint8Array + +/** Dynamic key resolver for consuming operations. */ +export interface GetKeyFunction { + /** + * Resolves a key for an unverified token. Throw if no suitable key can be resolved. + * + * @param token The consumed JWE or JWS token; none of its components have been verified. + */ + (protectedHeader: IProtectedHeader, token: IToken): Promise | KeyTypes +} + +/** Flattened JWS verification input. */ +export interface FlattenedJWSInput { + /** JWS Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + header?: JWSHeaderParameters + + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** Base64url-encoded UTF-8 JWS Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded signature or MAC. */ + signature: string +} + +/** General JWS verification input. */ +export interface GeneralJWSInput { + /** Base64url-encoded payload; with `b64: false`, supply an unencoded string or Uint8Array. */ + payload: string | Uint8Array + + /** + * The "signatures" member value MUST be an array of JSON objects. Each object represents a + * signature or MAC over the JWS Payload and the JWS Protected Header. + */ + signatures: Omit[] +} + +/** + * Flattened JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface FlattenedJWS extends Partial { + payload: string + signature: string +} + +/** + * General JWS JSON Serialization token. The payload is an empty string when the + * {@link https://www.rfc-editor.org/info/rfc7797/ unencoded payload option} is used. + */ +export interface GeneralJWS { + payload: string + signatures: Omit[] +} + +/** Header Parameters common to JWE and JWS. */ +export interface JoseHeaderParameters { + /** "kid" (Key ID) Header Parameter */ + kid?: string + + /** X.509 certificate SHA-1 thumbprint. */ + x5t?: string + + /** X.509 certificate chain. */ + x5c?: string[] + + /** X.509 certificate URL. */ + x5u?: string + + /** JWK Set URL. */ + jku?: string + + /** Public JWK only; private and symmetric key parameters are not permitted. */ + jwk?: Omit + + /** "typ" (Type) Header Parameter */ + typ?: string + + /** Content type. */ + cty?: string +} + +/** Recognized JWS Header Parameters; additional members may also be present. */ +export interface JWSHeaderParameters extends JoseHeaderParameters { + /** JWS "alg" (Algorithm) Header Parameter */ + alg?: JWSAlgorithm + + /** + * Controls payload encoding and the JWS signing input as defined by + * {@link https://www.rfc-editor.org/info/rfc7797/ RFC7797}. Set to `false` and list `b64` in + * `crit` to use an unencoded payload. + */ + b64?: boolean + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** Any other JWS Header member. */ + [propName: string]: unknown +} + +/** Recognized JWE Key Management-related Header Parameters. */ +export interface JWEKeyManagementHeaderParameters { + /** ECDH-ES Agreement PartyUInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apu?: Uint8Array + + /** ECDH-ES Agreement PartyVInfo bytes, used in ConcatKDF and added to the JOSE header. */ + apv?: Uint8Array + + /** PBES2 PBKDF2 iteration count, added to the JOSE header. */ + p2c?: number + + /** @deprecated For testing and vector validation only. */ + p2s?: Uint8Array + /** @deprecated For testing and vector validation only. */ + iv?: Uint8Array + /** @deprecated For testing and vector validation only. */ + epk?: CryptoKey | KeyObject +} + +/** Flattened JWE JSON Serialization token. */ +export interface FlattenedJWE { + /** + * Base64url-encoded additional authenticated data; integrity protected but not encrypted. Omit + * when empty. + */ + aad?: string + + /** Base64url-encoded ciphertext. */ + ciphertext: string + + /** Base64url-encoded encrypted key. Omit when empty. */ + encrypted_key?: string + + /** + * JWE Per-Recipient Unprotected Header as a JSON object. Not integrity protected; omit when + * empty. + */ + header?: JWEHeaderParameters + + /** Base64url-encoded initialization vector. Omit when empty. */ + iv?: string + + /** Base64url-encoded UTF-8 JWE Protected Header. Integrity protected; omit when empty. */ + protected?: string + + /** Base64url-encoded authentication tag. Omit when empty. */ + tag?: string + + /** JWE Shared Unprotected Header as a JSON object. Not integrity protected; omit when empty. */ + unprotected?: JWEHeaderParameters +} + +/** General JWE JSON Serialization token. */ +export interface GeneralJWE extends Omit { + recipients: Pick[] +} + +/** Recognized JWE Header Parameters; additional members may also be present. */ +export interface JWEHeaderParameters extends JoseHeaderParameters { + /** JWE "alg" (Algorithm) Header Parameter */ + alg?: JWEKeyManagementAlgorithm + + /** JWE "enc" (Encryption Algorithm) Header Parameter */ + enc?: JWEContentEncryptionAlgorithm + + /** Extension parameters that must be recognized. */ + crit?: string[] + + /** + * JWE compression algorithm. Only `"DEF"` (DEFLATE) is supported, requiring the runtime's + * `CompressionStream` / `DecompressionStream` APIs. + */ + zip?: 'DEF' | (string & {}) + + /** Any other JWE Header member. */ + [propName: string]: unknown +} + +/** Shared "crit" option for signing, verification, encryption, and decryption. */ +export interface CritOption { + /** + * Recognized "crit" (Critical) Header Parameter names. Set each value to `true` to require + * integrity protection, or `false` when protection is optional. The JWS `b64` extension is always + * recognized and processed. + * + * > Warning: Other extensions are only checked for syntax and optional integrity protection. Their presence + * > is not required by this option. You must check their presence and process them according to the + * > profile's validation steps after the operation succeeds. + */ + crit?: { + [propName: string]: boolean + } +} + +/** JWE Decryption options. */ +export interface DecryptOptions extends CritOption { + /** + * Accepted JWE "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret except PBES2, which must be explicitly allowed. + */ + keyManagementAlgorithms?: JWEKeyManagementAlgorithm[] + + /** + * Accepted JWE "enc" (Encryption Algorithm) Header Parameter values. Defaults to all algorithms + * applicable to the key or secret. + */ + contentEncryptionAlgorithms?: JWEContentEncryptionAlgorithm[] + + /** + * Maximum "p2c" (PBES2 Count) Header Parameter value, limiting PBKDF2 iterations and their + * computational expense. Defaults to 10000; must be a positive safe integer or `Infinity` to + * disable the limit. + */ + maxPBES2Count?: number + + /** + * Maximum decompressed plaintext size in bytes. Defaults to 250000; `0` rejects compressed JWEs, + * and `Infinity` disables the limit. Other values must be positive safe integers. + */ + maxDecompressedLength?: number +} + +/** JWE Encryption options. */ +export interface EncryptOptions extends CritOption {} + +/** JWT Claims Set verification options. */ +export interface JWTClaimVerificationOptions { + /** Expected JWT "aud" (Audience) Claim value(s). Requires the claim to be present. */ + audience?: string | string[] + + /** + * Clock skew tolerance in seconds or a duration string (e.g. "5 seconds"). Applies to the "nbf" + * (Not Before) and "exp" (Expiration Time) claims, and to "iat" (Issued At) when + * {@link maxTokenAge} is set. + */ + clockTolerance?: string | number + + /** Expected JWT "iss" (Issuer) Claim value(s). Requires the claim to be present. */ + issuer?: string | string[] + + /** + * Maximum time since the JWT "iat" (Issued At) Claim, in seconds or a duration string (e.g. "2 + * hours"). Requires the claim to be present. + */ + maxTokenAge?: string | number + + /** Expected JWT "sub" (Subject) Claim value. Requires the claim to be present. */ + subject?: string + + /** Expected JWT "typ" (Type) Header Parameter value. Requires the parameter to be present. */ + typ?: string + + /** Date for NumericDate comparisons. Defaults to `new Date()`. */ + currentDate?: Date + + /** + * Additional claim names required in the JWT Claims Set. The {@link issuer}, {@link audience}, + * {@link subject}, and {@link maxTokenAge} options independently require "iss", "aud", "sub", and + * "iat", respectively. + */ + requiredClaims?: string[] +} + +/** JWS Verification options. */ +export interface VerifyOptions extends CritOption { + /** + * Accepted JWS "alg" (Algorithm) Header Parameter values. Defaults to all algorithms applicable + * to the key or secret. Unsecured JWTs (`alg: "none"`) are never accepted. + */ + algorithms?: JWSAlgorithm[] +} + +/** JWS Signing options. */ +export interface SignOptions extends CritOption {} + +/** Recognized JWT Claims Set members; additional members may also be present. */ +export interface JWTPayload { + /** JWT Issuer */ + iss?: string + + /** JWT Subject */ + sub?: string + + /** JWT Audience */ + aud?: string | string[] + + /** JWT ID */ + jti?: string + + /** Not valid before this Unix timestamp in seconds. */ + nbf?: number + + /** Expiration Unix timestamp in seconds. */ + exp?: number + + /** Issued-at Unix timestamp in seconds. */ + iat?: number + + /** Any other JWT Claim Set member. */ + [propName: string]: unknown +} + +/** Flattened JWE JSON Serialization decryption result. */ +export interface FlattenedDecryptResult { + /** JWE Additional Authenticated Data, integrity protected but not encrypted. */ + additionalAuthenticatedData?: Uint8Array + + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader?: JWEHeaderParameters + + /** JWE Shared Unprotected Header. Not integrity protected. */ + sharedUnprotectedHeader?: JWEHeaderParameters + + /** JWE Per-Recipient Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWEHeaderParameters +} + +/** General JWE JSON Serialization decryption result. */ +export interface GeneralDecryptResult extends FlattenedDecryptResult {} + +/** Compact JWE decryption result. */ +export interface CompactDecryptResult { + /** Plaintext. */ + plaintext: Uint8Array + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Flattened JWS JSON Serialization verification result. */ +export interface FlattenedVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader?: JWSHeaderParameters + + /** JWS Unprotected Header. Not integrity protected. */ + unprotectedHeader?: JWSHeaderParameters +} + +/** General JWS JSON Serialization verification result. */ +export interface GeneralVerifyResult extends FlattenedVerifyResult {} + +/** Compact JWS verification result. */ +export interface CompactVerifyResult { + /** JWS Payload. */ + payload: Uint8Array + + /** JWS Protected Header. */ + protectedHeader: CompactJWSHeaderParameters +} + +/** Signed JSON Web Token (JWT) verification result. */ +export interface JWTVerifyResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWS Protected Header. */ + protectedHeader: JWTHeaderParameters +} + +/** Encrypted JSON Web Token (JWT) decryption result. */ +export interface JWTDecryptResult { + /** JWT Claims Set. */ + payload: PayloadType & + JWTPayload & + ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never) + + /** JWE Protected Header. */ + protectedHeader: CompactJWEHeaderParameters +} + +/** Key resolver result metadata. */ +export interface ResolvedKey { + /** The resolved key, present only when a key resolver is used. */ + key: KeyType +} + +/** Recognized Compact JWS Header Parameters; additional members may also be present. */ +export interface CompactJWSHeaderParameters extends JWSHeaderParameters { + alg: JWSAlgorithm +} + +/** Recognized signed JWT Header Parameters; additional members may also be present. */ +export interface JWTHeaderParameters extends CompactJWSHeaderParameters { + b64?: boolean +} + +/** Recognized Compact JWE Header Parameters; additional members may also be present. */ +export interface CompactJWEHeaderParameters extends JWEHeaderParameters { + alg: JWEKeyManagementAlgorithm + enc: JWEContentEncryptionAlgorithm +} + +/** JSON Web Key Set. */ +export interface JSONWebKeySet { + keys: JWK[] +} + +/** Node.js {@link !KeyObject} representation accepted as key input. */ +export interface KeyObject { + type: string +} + +/** The runtime's Web Crypto {@link !CryptoKey} representation accepted as key input. */ +export type CryptoKey = typeof globalThis extends { + crypto: { subtle: { generateKey(...args: any[]): Promise } } +} + ? Extract + : CryptoKeyStructuralFallback + +/** Structural fallback used when the host {@link !CryptoKey} type cannot be inferred. */ +export interface CryptoKeyStructuralFallback { + readonly algorithm: { name: string } + readonly extractable: boolean + readonly type: string + readonly usages: string[] +} + +/** Shared fluent API for JWT-producing classes. */ +export interface ProduceJWT { + /** + * Set the "iss" (Issuer) Claim. + * + * @param issuer "Issuer" Claim value to set on the JWT Claims Set. + */ + setIssuer(issuer: string): this + + /** + * Set the "sub" (Subject) Claim. + * + * @param subject "sub" (Subject) Claim value to set on the JWT Claims Set. + */ + setSubject(subject: string): this + + /** + * Set the "aud" (Audience) Claim. + * + * @param audience "aud" (Audience) Claim value to set on the JWT Claims Set. + */ + setAudience(audience: string | string[]): this + + /** + * Set the "jti" (JWT ID) Claim. + * + * @param jwtId "jti" (JWT ID) Claim value to set on the JWT Claims Set. + */ + setJti(jwtId: string): this + + /** + * Set the "nbf" (Not Before) Claim. Numbers are Unix timestamps in seconds; Dates are converted + * to seconds. Strings are relative to now, using seconds, minutes, hours, days, weeks, or years + * (365.25 days; no months). Prefix `-` or suffix `ago` subtracts the duration. + * + * @param input "nbf" (Not Before) Claim value as a timestamp, Date, or relative duration. + */ + setNotBefore(input: number | string | Date): this + + /** + * Set the "exp" (Expiration Time) Claim. Accepts a Unix timestamp in seconds, a Date, or a + * duration relative to now using the same formats as {@link setNotBefore}. + * + * @param input "exp" (Expiration Time) Claim value as a timestamp, Date, or relative duration. + */ + setExpirationTime(input: number | string | Date): this + + /** + * Set the "iat" (Issued At) Claim. Defaults to the current Unix timestamp in seconds. Accepts a + * Unix timestamp in seconds, a Date, or a duration relative to now using the same formats as + * {@link setNotBefore}. + * + * @param input "iat" (Issued At) Claim value as a timestamp, Date, or relative duration. + */ + setIssuedAt(input?: number | string | Date): this +} diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts new file mode 100644 index 000000000..6d8a665fa --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/base64url.d.ts @@ -0,0 +1,16 @@ +/** + * Decodes base64url text or its UTF-8 bytes. + * + * @param input Base64url-encoded string or its UTF-8 bytes. + * @returns The decoded bytes. + * @throws {TypeError} If the input is not valid base64url. Standard Base64 `+` and `/` are + * rejected. + */ +export declare function decode(input: Uint8Array | string): Uint8Array; +/** + * Encodes unpadded base64url; strings are first encoded as UTF-8. + * + * @param input Bytes or a string to encode. Strings are first encoded as UTF-8. + * @returns The unpadded base64url representation of the input. + */ +export declare function encode(input: Uint8Array | string): string; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts new file mode 100644 index 000000000..63ce31f0e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_jwt.d.ts @@ -0,0 +1,8 @@ +import type * as t from '../types.d.ts'; +/** + * Decodes the Claims Set of a JWT in Compact JWS serialization without checking its signature or + * validating claim types and values. + * + * @returns The parsed JWT Claims Set. + */ +export declare function decodeJwt(jwt: string): PayloadType & t.JWTPayload & ([PayloadType] extends [object] ? unknown : unknown extends PayloadType ? unknown : never); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts new file mode 100644 index 000000000..2d74643d3 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/decode_protected_header.d.ts @@ -0,0 +1,10 @@ +import type * as t from '../types.d.ts'; +/** JWE and JWS Header Parameters returned by {@link decodeProtectedHeader}. */ +export type ProtectedHeaderParameters = t.JWSHeaderParameters & t.JWEHeaderParameters; +/** + * Decodes the Protected Header of a JWE, JWS, or JWT without authenticating the token. + * + * @param token Compact token or JSON serialization object with a `protected` member. + * @returns The parsed Protected Header. + */ +export declare function decodeProtectedHeader(token: string | object): ProtectedHeaderParameters; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts new file mode 100644 index 000000000..930a29b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/types/util/errors.d.ts @@ -0,0 +1,182 @@ +import type * as t from '../types.d.ts'; +/** Stable error codes used by this module. */ +export type JOSEErrorCode = 'ERR_JOSE_ALG_NOT_ALLOWED' | 'ERR_JOSE_GENERIC' | 'ERR_JOSE_NOT_SUPPORTED' | 'ERR_JWE_DECRYPTION_FAILED' | 'ERR_JWE_INVALID' | 'ERR_JWK_INVALID' | 'ERR_JWKS_INVALID' | 'ERR_JWKS_MULTIPLE_MATCHING_KEYS' | 'ERR_JWKS_NO_MATCHING_KEY' | 'ERR_JWKS_TIMEOUT' | 'ERR_JWS_INVALID' | 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED' | 'ERR_JWT_CLAIM_VALIDATION_FAILED' | 'ERR_JWT_EXPIRED' | 'ERR_JWT_INVALID'; +/** Shared details of JWT claim or header validation failures. */ +export interface JWTClaimValidationFailure { + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** The parsed JWT Claims Set; validation of other claims may be incomplete. */ + payload: t.JWTPayload; +} +/** Reason codes produced by JWT Claims Set validation. */ +export type JWTClaimValidationReason = 'check_failed' | 'invalid' | 'mismatch' | 'missing' | 'unspecified' | (string & {}); +/** Base class for JOSE errors. */ +export declare class JOSEError extends Error { + /** Stable code identifying the error class. */ + static code: JOSEErrorCode | (string & {}); + /** Stable code identifying the error class. Use {@link AnyJOSEError} to narrow subclasses by code. */ + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** + * Thrown when JWT claim or header validation fails. Expiration is reported separately as + * {@link JWTExpired}. + */ +export declare class JWTClaimValidationFailed extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when a JWT has expired or exceeds the configured maximum token age. */ +export declare class JWTExpired extends JOSEError implements JWTClaimValidationFailure { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + /** The {@link JWTClaimValidationFailure} details carried by this error. */ + cause: JWTClaimValidationFailure; + /** Claim or header that failed validation. */ + claim: string; + /** Reason code for the validation failure. */ + reason: JWTClaimValidationReason; + /** + * The parsed JWT Claims Set; validation of other claims may be incomplete. With + * {@link jwt/verify.jwtVerify jwtVerify} and {@link jwt/decrypt.jwtDecrypt jwtDecrypt}, token + * authentication precedes claim validation. {@link jwt/unsecured.UnsecuredJWT.decode} does not + * authenticate tokens. + */ + payload: t.JWTPayload; + constructor(message: string, payload: t.JWTPayload, claim?: string, reason?: JWTClaimValidationReason); +} +/** Thrown when an algorithm is disallowed by configuration. */ +export declare class JOSEAlgNotAllowed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a feature or algorithm is unsupported. */ +export declare class JOSENotSupported extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when JWE ciphertext decryption or authentication fails. */ +export declare class JWEDecryptionFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when a JWE is invalid. */ +export declare class JWEInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWS is invalid. */ +export declare class JWSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWT is invalid. */ +export declare class JWTInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWK is invalid. */ +export declare class JWKInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when a JWKS is invalid. */ +export declare class JWKSInvalid extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); +} +/** Thrown when no keys match in a JWKS. */ +export declare class JWKSNoMatchingKey extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when multiple keys match in a JWKS. */ +export declare class JWKSMultipleMatchingKeys extends JOSEError { + /** + * Yields public keys matching the JWS Header so verification can be attempted with each. See the + * {@link jwks/remote.createRemoteJWKSet createRemoteJWKSet} and + * {@link jwks/local.createLocalJWKSet createLocalJWKSet} examples. Empty on manually constructed + * errors. + */ + [Symbol.asyncIterator]: () => AsyncIterableIterator; + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when fetching a remote JWKS times out. */ +export declare class JWKSTimeout extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Thrown when JWS signature verification fails. */ +export declare class JWSSignatureVerificationFailed extends JOSEError { + static code: JOSEErrorCode | (string & {}); + code: JOSEErrorCode | (string & {}); + constructor(message?: string, options?: { + cause?: unknown; + }); +} +/** Errors thrown during JWT Claims Set validation. */ +export type JWTClaimValidationError = JWTClaimValidationFailed | JWTExpired; +/** + * Discriminated union of specific {@link JOSEError} subclasses, narrowed by `code`. Excludes base + * {@link JOSEError} instances; use `instanceof JOSEError` to catch all module errors. + */ +export type AnyJOSEError = (JOSEAlgNotAllowed & { + code: 'ERR_JOSE_ALG_NOT_ALLOWED'; +}) | (JOSENotSupported & { + code: 'ERR_JOSE_NOT_SUPPORTED'; +}) | (JWEDecryptionFailed & { + code: 'ERR_JWE_DECRYPTION_FAILED'; +}) | (JWEInvalid & { + code: 'ERR_JWE_INVALID'; +}) | (JWKInvalid & { + code: 'ERR_JWK_INVALID'; +}) | (JWKSInvalid & { + code: 'ERR_JWKS_INVALID'; +}) | (JWKSMultipleMatchingKeys & { + code: 'ERR_JWKS_MULTIPLE_MATCHING_KEYS'; +}) | (JWKSNoMatchingKey & { + code: 'ERR_JWKS_NO_MATCHING_KEY'; +}) | (JWKSTimeout & { + code: 'ERR_JWKS_TIMEOUT'; +}) | (JWSInvalid & { + code: 'ERR_JWS_INVALID'; +}) | (JWSSignatureVerificationFailed & { + code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED'; +}) | (JWTClaimValidationFailed & { + code: 'ERR_JWT_CLAIM_VALIDATION_FAILED'; +}) | (JWTExpired & { + code: 'ERR_JWT_EXPIRED'; +}) | (JWTInvalid & { + code: 'ERR_JWT_INVALID'; +}); diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js new file mode 100644 index 000000000..a41eeb626 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/index.js @@ -0,0 +1,70 @@ +import { compactDecrypt } from "./jwe/compact/decrypt.js"; +import { flattenedDecrypt } from "./jwe/flattened/decrypt.js"; +import { generalDecrypt } from "./jwe/general/decrypt.js"; +import { GeneralEncrypt } from "./jwe/general/encrypt.js"; +import { compactVerify } from "./jws/compact/verify.js"; +import { flattenedVerify } from "./jws/flattened/verify.js"; +import { generalVerify } from "./jws/general/verify.js"; +import { jwtVerify } from "./jwt/verify.js"; +import { jwtDecrypt } from "./jwt/decrypt.js"; +import { CompactEncrypt } from "./jwe/compact/encrypt.js"; +import { FlattenedEncrypt } from "./jwe/flattened/encrypt.js"; +import { CompactSign } from "./jws/compact/sign.js"; +import { FlattenedSign } from "./jws/flattened/sign.js"; +import { GeneralSign } from "./jws/general/sign.js"; +import { SignJWT } from "./jwt/sign.js"; +import { EncryptJWT } from "./jwt/encrypt.js"; +import { calculateJwkThumbprint, calculateJwkThumbprintUri } from "./jwk/thumbprint.js"; +import { EmbeddedJWK } from "./jwk/embedded.js"; +import { createLocalJWKSet } from "./jwks/local.js"; +import { createRemoteJWKSet, jwksCache, customFetch } from "./jwks/remote.js"; +import { UnsecuredJWT } from "./jwt/unsecured.js"; +import { exportPKCS8, exportSPKI, exportJWK } from "./key/export.js"; +import { importSPKI, importPKCS8, importX509, importJWK } from "./key/import.js"; +import { decodeProtectedHeader } from "./util/decode_protected_header.js"; +import { decodeJwt } from "./util/decode_jwt.js"; +import * as errors from "./util/errors.js"; +import { generateKeyPair } from "./key/generate_key_pair.js"; +import { generateSecret } from "./key/generate_secret.js"; +import * as base64url from "./util/base64url.js"; +const cryptoRuntime = "WebCryptoAPI"; +export { + CompactEncrypt, + CompactSign, + EmbeddedJWK, + EncryptJWT, + FlattenedEncrypt, + FlattenedSign, + GeneralEncrypt, + GeneralSign, + SignJWT, + UnsecuredJWT, + base64url, + calculateJwkThumbprint, + calculateJwkThumbprintUri, + compactDecrypt, + compactVerify, + createLocalJWKSet, + createRemoteJWKSet, + cryptoRuntime, + customFetch, + decodeJwt, + decodeProtectedHeader, + errors, + exportJWK, + exportPKCS8, + exportSPKI, + flattenedDecrypt, + flattenedVerify, + generalDecrypt, + generalVerify, + generateKeyPair, + generateSecret, + importJWK, + importPKCS8, + importSPKI, + importX509, + jwksCache, + jwtDecrypt, + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js new file mode 100644 index 000000000..3c20fe5df --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/decrypt.js @@ -0,0 +1,7 @@ +import { prepareDecrypt, decryptCompact } from "../../lib/jwe_decrypt.js"; +async function compactDecrypt(jwe, key, options) { + return decryptCompact(jwe, prepareDecrypt(options), key); +} +export { + compactDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js new file mode 100644 index 000000000..a500eccd6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/compact/encrypt.js @@ -0,0 +1,26 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { compactJWE, createJWE } from "../../lib/jwe_encrypt.js"; +class CompactEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + async encrypt(key, options) { + return compactJWE(await createJWE([...this.#input], key, options)); + } +} +export { + CompactEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js new file mode 100644 index 000000000..cba368907 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js @@ -0,0 +1,15 @@ +import { JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { prepareDecrypt, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +async function flattenedDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("Flattened JWE must be an object"); + const shared = snapshotSharedJWE(jwe), [recipient, , error] = snapshotRecipientJWE(jwe); + if (!recipient) + throw error; + const snapshot = { ...shared, ...recipient }; + return checkRecipient(snapshot), decryptJWE(snapshot, prepareDecrypt(options), key); +} +export { + flattenedDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js new file mode 100644 index 000000000..7ee1b91ed --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js @@ -0,0 +1,35 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { createJWE } from "../../lib/jwe_encrypt.js"; +class FlattenedEncrypt { + #input; + constructor(plaintext) { + assertUint8Array(plaintext, "plaintext"), this.#input = [plaintext]; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#input[3], "setSharedUnprotectedHeader"), this.#input[3] = sharedUnprotectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#input[4] = aad, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + async encrypt(key, options) { + return createJWE([...this.#input], key, options); + } +} +export { + FlattenedEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js new file mode 100644 index 000000000..dfdb66003 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/decrypt.js @@ -0,0 +1,40 @@ +import { prepareDecrypt, shareJWE, decryptJWE, checkRecipient, snapshotSharedJWE, snapshotRecipientJWE } from "../../lib/jwe_decrypt.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { JWE, isJWECEKTransport } from "../../lib/jwe_algorithms.js"; +async function generalDecrypt(jwe, key, options) { + if (!isObject(jwe)) + throw new JWEInvalid("General JWE must be an object"); + const inputRecipients = jwe.recipients; + if (!Array.isArray(inputRecipients)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + const recipients = Array.from(inputRecipients); + if (!recipients.every(isObject)) + throw new JWEInvalid("JWE Recipients missing or incorrect type"); + if (!recipients.length) + throw new JWEInvalid("JWE Recipients has no members"); + let shared, sharedJwe, token; + try { + shared = prepareDecrypt(options), sharedJwe = snapshotSharedJWE(jwe), token = shareJWE(sharedJwe); + } catch { + throw new JWEDecryptionFailed(); + } + const recipientSnapshots = recipients.map((recipient) => snapshotRecipientJWE(recipient)); + if (recipients.length > 1) + for (const [, headerAlg] of recipientSnapshots) { + const alg = token[0]?.alg ?? headerAlg ?? sharedJwe.unprotected?.alg, algEntry = typeof alg == "string" ? JWE[alg] : void 0; + if (algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + } + for (const [recipient] of recipientSnapshots) + if (recipient) + try { + const flattened = { ...sharedJwe, ...recipient }; + return checkRecipient(flattened), await decryptJWE(flattened, shared, key, token); + } catch { + } + throw new JWEDecryptionFailed(); +} +export { + generalDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js new file mode 100644 index 000000000..9fe068a8a --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwe/general/encrypt.js @@ -0,0 +1,105 @@ +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +import { JWEInvalid } from "../../util/errors.js"; +import { generateCek } from "../../lib/content_encryption.js"; +import { encode as b64u } from "../../util/base64url.js"; +import { checkDisjoint, checkEncryptHeaders, encryptJWE } from "../../lib/jwe_encrypt.js"; +import { encryptKeyManagement } from "../../lib/key_management.js"; +import { isJWECEKTransport, jweAlgorithm } from "../../lib/jwe_algorithms.js"; +class IndividualRecipient { + #parent; + state; + constructor(enc, key, crit) { + this.#parent = enc, this.state = [void 0, void 0, key, crit]; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[0], "setUnprotectedHeader"), this.state[0] = unprotectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.state[1], "setKeyManagementParameters"), this.state[1] = parameters, this; + } + addRecipient(...args) { + return this.#parent.addRecipient(...args); + } + encrypt(...args) { + return this.#parent.encrypt(...args); + } + done() { + return this.#parent; + } +} +class GeneralEncrypt { + #plaintext; + #recipients = []; + #protectedHeader; + #unprotectedHeader; + #aad; + constructor(plaintext) { + this.#plaintext = plaintext; + } + addRecipient(key, options) { + const recipient = new IndividualRecipient(this, key, options?.crit); + return this.#recipients.push(recipient), recipient; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + setSharedUnprotectedHeader(sharedUnprotectedHeader) { + return assertNotSet(this.#unprotectedHeader, "setSharedUnprotectedHeader"), this.#unprotectedHeader = sharedUnprotectedHeader, this; + } + setAdditionalAuthenticatedData(aad) { + return this.#aad = aad, this; + } + async encrypt() { + if (!this.#recipients.length) + throw new JWEInvalid("at least one recipient must be added"); + assertUint8Array(this.#plaintext, "plaintext"); + const multiple = this.#recipients.length > 1; + let enc, protectedHeader = this.#protectedHeader, sharedUnprotectedHeader = this.#unprotectedHeader; + const recipients = []; + for (const recipient of this.#recipients) { + const [unprotectedHeader, keyManagementParameters, key, crit] = recipient.state, input = [ + this.#plaintext, + protectedHeader, + unprotectedHeader, + sharedUnprotectedHeader, + this.#aad, + void 0, + void 0, + keyManagementParameters, + crit, + multiple + ], headers = checkEncryptHeaders(input, void 0, recipients.length > 0); + recipients.length || (protectedHeader = input[1], sharedUnprotectedHeader = input[3]), recipients.push([input, headers, key]); + const [{ alg, enc: recipientEnc }, , algEntry] = headers; + if (multiple && algEntry && !isJWECEKTransport(algEntry)) + throw new JWEInvalid(`"${alg}" alg may only have a single recipient`); + if (!enc) + enc = recipientEnc; + else if (enc !== recipientEnc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must be the same for all recipients'); + } + for (const [, headers] of recipients) + headers[2] ??= jweAlgorithm(headers[0].alg); + const [firstInput, firstHeaders, firstKey] = recipients[0], cek = multiple ? generateCek(firstHeaders[1]) : void 0; + firstInput[5] = cek; + const { encrypted_key, header, ...shared } = await encryptJWE(firstInput, firstHeaders, firstKey), jwe = { + ...shared, + recipients: [{}] + }; + encrypted_key && (jwe.recipients[0].encrypted_key = encrypted_key), header && (jwe.recipients[0].header = header); + for (let i = 1; i < recipients.length; i++) { + const [input, [joseHeader, encEntry, algEntry], key] = recipients[i], unprotectedHeader = input[2], [, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, cek, input[7]), target = { + encrypted_key: b64u(encryptedKey) + }; + if (unprotectedHeader || parameters) { + const header2 = { ...unprotectedHeader, ...parameters }; + parameters && checkDisjoint(input[1], header2, input[3]), target.header = header2; + } + jwe.recipients.push(target); + } + return jwe; + } +} +export { + GeneralEncrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js new file mode 100644 index 000000000..1e1dd89c4 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/embedded.js @@ -0,0 +1,30 @@ +import { jwkToKey, normalizeJwk } from "../lib/key.js"; +import { jwsAlgorithm } from "../lib/jws_algorithms.js"; +import { isObject } from "../lib/validate.js"; +import { JWSInvalid } from "../util/errors.js"; +async function EmbeddedJWK(protectedHeader, token) { + const joseHeader = { + ...protectedHeader, + ...token?.header + }; + if (!isObject(joseHeader.jwk)) + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a JSON object'); + let jwk; + try { + jwk = normalizeJwk(joseHeader.jwk); + } catch (cause) { + throw new JWSInvalid("Invalid Embedded JWK", { cause }); + } + const entry = jwsAlgorithm(joseHeader.alg); + if (jwk.use !== void 0 && jwk.use !== "sig") + throw new JWSInvalid('Invalid Embedded JWK, its "use" must be "sig" when present'); + if (jwk.alg !== void 0 && jwk.alg !== entry.alg) + throw new JWSInvalid(`Invalid Embedded JWK, its "alg" must be "${entry.alg}" when present`); + const key = await jwkToKey(entry, jwk, !0); + if (key.type !== "public") + throw new JWSInvalid('"jwk" (JSON Web Key) Header Parameter must be a public key'); + return key; +} +export { + EmbeddedJWK +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js new file mode 100644 index 000000000..ec93e222e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwk/thumbprint.js @@ -0,0 +1,55 @@ +import { digest, encode } from "../lib/buffer_utils.js"; +import { encode as b64u } from "../util/base64url.js"; +import { JOSENotSupported, JWKInvalid } from "../util/errors.js"; +import { isKeyLike, invalidKeyInput, snapshotJwk } from "../lib/key.js"; +import { isObject } from "../lib/validate.js"; +import { exportJWK } from "../key/export.js"; +const check = (value, description) => { + if (typeof value != "string" || !value) + throw new JWKInvalid(`${description} missing or invalid`); +}; +async function calculateJwkThumbprint(key, digestAlgorithm) { + let jwk; + if (isObject(key)) { + if (jwk = snapshotJwk(key), typeof jwk.kty != "string") + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + } else if (isKeyLike(key)) + jwk = snapshotJwk(await exportJWK(key)); + else + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key")); + if (digestAlgorithm ??= "sha256", digestAlgorithm !== "sha256" && digestAlgorithm !== "sha384" && digestAlgorithm !== "sha512") + throw new TypeError('digestAlgorithm must one of "sha256", "sha384", or "sha512"'); + let components; + switch (jwk.kty) { + case "AKP": + check(jwk.alg, '"alg" (Algorithm) Parameter'), check(jwk.pub, '"pub" (Public key) Parameter'), components = { alg: jwk.alg, kty: jwk.kty, pub: jwk.pub }; + break; + case "EC": + check(jwk.crv, '"crv" (Curve) Parameter'), check(jwk.x, '"x" (X Coordinate) Parameter'), check(jwk.y, '"y" (Y Coordinate) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }; + break; + case "OKP": + check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter'), check(jwk.x, '"x" (Public Key) Parameter'), components = { crv: jwk.crv, kty: jwk.kty, x: jwk.x }; + break; + case "RSA": + check(jwk.e, '"e" (Exponent) Parameter'), check(jwk.n, '"n" (Modulus) Parameter'), components = { e: jwk.e, kty: jwk.kty, n: jwk.n }; + break; + case "oct": + if (typeof jwk.k != "string") + throw new JWKInvalid('"k" (Key Value) Parameter missing or invalid'); + components = { k: jwk.k, kty: jwk.kty }; + break; + default: + throw new JOSENotSupported('"kty" (Key Type) Parameter missing or unsupported'); + } + const data = encode(JSON.stringify(components)); + return b64u(await digest(digestAlgorithm, data)); +} +async function calculateJwkThumbprintUri(key, digestAlgorithm) { + digestAlgorithm ??= "sha256"; + const thumbprint = await calculateJwkThumbprint(key, digestAlgorithm); + return `urn:ietf:params:oauth:jwk-thumbprint:sha-${digestAlgorithm.slice(-3)}:${thumbprint}`; +} +export { + calculateJwkThumbprint, + calculateJwkThumbprintUri +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js new file mode 100644 index 000000000..448757265 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/local.js @@ -0,0 +1,59 @@ +import { jwkToKey, snapshotJwk } from "../lib/key.js"; +import { JWS } from "../lib/jws_algorithms.js"; +import { JWKSInvalid, JOSENotSupported, JWKSNoMatchingKey, JWKSMultipleMatchingKeys } from "../util/errors.js"; +import { isJwkSet } from "../lib/validate.js"; +function isUsableJWK(jwk, entry, alg, kid) { + const { kty, key_ops: keyOps, ext, kid: jwkKid, alg: jwkAlg, use, crv } = jwk; + return (ext === void 0 || typeof ext == "boolean") && (keyOps === void 0 || Array.isArray(keyOps) && keyOps.every((operation, index) => typeof operation == "string" && keyOps.indexOf(operation) === index) && keyOps.includes("verify")) && entry.kty.includes(kty) && (kid === void 0 || typeof kid == "string" && kid === jwkKid) && (jwkAlg === void 0 ? kty !== "AKP" : alg === jwkAlg) && (use === void 0 || use === "sig") && (!entry.crv || crv === entry.crv); +} +async function importWithAlgCache(cache, jwk, entry) { + const cached = cache.get(jwk) || cache.set(jwk, {}).get(jwk), { alg } = entry; + if (cached[alg] === void 0) { + const pending = jwkToKey(entry, jwk, !0).then((key) => { + if (key.type !== "public") + throw new JWKSInvalid("JSON Web Key Set members must be public keys"); + return cached[alg] = key, key; + }).catch((error) => { + throw cached[alg] === pending && delete cached[alg], error; + }); + cached[alg] = pending; + } + return cached[alg]; +} +function createLocalJWKSet(jwks) { + let snapshot; + try { + snapshot = structuredClone(jwks); + } catch { + } + if (!isJwkSet(snapshot)) + throw new JWKSInvalid("JSON Web Key Set malformed"); + const metadata = snapshot.keys.map((jwk) => { + const normalized = snapshotJwk(jwk); + return Array.isArray(normalized.key_ops) && (normalized.key_ops = [...normalized.key_ops]), normalized; + }), cached = /* @__PURE__ */ new WeakMap(); + return Object.defineProperty(async (protectedHeader, token) => { + const { alg, kid } = { ...protectedHeader, ...token?.header }, entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry || entry.secret) + throw new JOSENotSupported('Unsupported "alg" value for a JSON Web Key Set'); + const candidates = snapshot.keys.filter((_, index) => isUsableJWK(metadata[index], entry, alg, kid)), { 0: jwk, length } = candidates; + if (!length) + throw new JWKSNoMatchingKey(); + if (length !== 1) { + const error = new JWKSMultipleMatchingKeys(); + throw error[Symbol.asyncIterator] = async function* () { + for (const jwk2 of candidates) + try { + yield await importWithAlgCache(cached, jwk2, entry); + } catch { + } + }, error; + } + return importWithAlgCache(cached, jwk, entry); + }, "jwks", { + value: () => structuredClone(snapshot) + }); +} +export { + createLocalJWKSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js new file mode 100644 index 000000000..39519f27b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwks/remote.js @@ -0,0 +1,101 @@ +import { JOSEError, JWKSNoMatchingKey, JWKSTimeout } from "../util/errors.js"; +import { createLocalJWKSet } from "./local.js"; +import { isJwkSet } from "../lib/validate.js"; +function isCloudflareWorkers() { + return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel"; +} +let USER_AGENT; +(typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12"); +const customFetch = /* @__PURE__ */ Symbol(); +async function fetchJwks(url, headers, signal, fetchImpl = fetch) { + const response = await fetchImpl(url, { + method: "GET", + signal, + redirect: "manual", + headers + }).catch((err) => { + throw err.name === "TimeoutError" ? new JWKSTimeout() : err; + }); + if (response.status !== 200) + throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response"); + try { + return await response.json(); + } catch { + throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON"); + } +} +const jwksCache = /* @__PURE__ */ Symbol(); +function isFreshFor(timestamp, duration) { + return Number.isFinite(timestamp) && Date.now() < timestamp + duration; +} +function validateDuration(value, fallback, option) { + if (Number.isNaN(value)) + throw new TypeError(`"${option}" option must not be NaN`); + return typeof value == "number" ? value : fallback; +} +function createRemoteJWKSet(url, options) { + if (!(url instanceof URL)) + throw new TypeError("url must be an instance of URL"); + const href = new URL(url.href).href, opts = options ?? {}, timeoutOption = opts.timeoutDuration; + if (typeof timeoutOption == "number" && (!Number.isInteger(timeoutOption) || timeoutOption < 0)) + throw new TypeError('"timeoutDuration" option must be a non-negative integer'); + const timeoutDuration = typeof timeoutOption == "number" ? timeoutOption : 5e3, cooldownDuration = validateDuration(opts.cooldownDuration, 3e4, "cooldownDuration"), cacheMaxAge = validateDuration(opts.cacheMaxAge, 6e5, "cacheMaxAge"), headers = new Headers(opts.headers); + USER_AGENT && !headers.has("User-Agent") && headers.set("User-Agent", USER_AGENT), headers.has("accept") || headers.set("accept", "application/json, application/jwk-set+json"); + const fetchImpl = opts[customFetch], cache = opts[jwksCache]; + let jwksTimestamp, pendingFetch, reloadSequence = 0, appliedSequence = 0, local; + if (cache && typeof cache == "object") { + const { uat, jwks } = cache; + isFreshFor(uat, cacheMaxAge) && isJwkSet(jwks) && (jwksTimestamp = uat, local = createLocalJWKSet(jwks)); + } + const reload = async () => { + if (pendingFetch && isCloudflareWorkers() && (pendingFetch = void 0), !pendingFetch) { + const sequence = ++reloadSequence, current = pendingFetch = fetchJwks(href, headers, AbortSignal.timeout(timeoutDuration), fetchImpl).then((json) => { + const next = createLocalJWKSet(json); + if (sequence <= appliedSequence) + return; + local = next; + const updatedAt = Date.now(); + cache && (cache.uat = updatedAt, cache.jwks = json), jwksTimestamp = updatedAt, appliedSequence = sequence; + }).finally(() => { + pendingFetch === current && (pendingFetch = void 0); + }); + } + await pendingFetch; + }; + return Object.defineProperties(async (protectedHeader, token) => { + (!local || !isFreshFor(jwksTimestamp, cacheMaxAge)) && await reload(); + try { + return await local(protectedHeader, token); + } catch (err) { + if (err instanceof JWKSNoMatchingKey && !isFreshFor(jwksTimestamp, cooldownDuration)) + return await reload(), local(protectedHeader, token); + throw err; + } + }, { + coolingDown: { + get: () => isFreshFor(jwksTimestamp, cooldownDuration), + enumerable: !0 + }, + fresh: { + get: () => isFreshFor(jwksTimestamp, cacheMaxAge), + enumerable: !0 + }, + reload: { + value: reload, + enumerable: !0 + }, + reloading: { + get: () => !!pendingFetch, + enumerable: !0 + }, + jwks: { + value: () => local?.jwks(), + enumerable: !0 + } + }); +} +export { + createRemoteJWKSet, + customFetch, + jwksCache +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js new file mode 100644 index 000000000..f3288d2f0 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/sign.js @@ -0,0 +1,20 @@ +import { createCompactSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class CompactSign { + #payload; + #protectedHeader; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#payload = payload; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(this.#payload, this.#protectedHeader, options?.crit, key, () => { + throw new TypeError("use the flattened module for creating JWS with b64: false"); + }); + } +} +export { + CompactSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js new file mode 100644 index 000000000..49625ee85 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/compact/verify.js @@ -0,0 +1,8 @@ +import { prepareVerify, verifyCompact } from "../../lib/jws_verify.js"; +async function compactVerify(jws, key, options) { + const [result] = await verifyCompact(jws, prepareVerify(options), key); + return result; +} +export { + compactVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js new file mode 100644 index 000000000..7e321402f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/sign.js @@ -0,0 +1,23 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class FlattenedSign { + #input; + constructor(payload) { + assertUint8Array(payload, "payload"), this.#input = [payload]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.#input[2], "setUnprotectedHeader"), this.#input[2] = unprotectedHeader, this; + } + async sign(key, options) { + const input = [...this.#input]; + input[3] = options?.crit; + const [jws] = await createSignature(input, key); + return jws; + } +} +export { + FlattenedSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js new file mode 100644 index 000000000..f4a7c9541 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/flattened/verify.js @@ -0,0 +1,12 @@ +import { JWSInvalid } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +import { encodeJsonUnencodedPayload, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +async function flattenedVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("Flattened JWS must be an object"); + const snapshot = snapshotJws(jws), [result] = await verifySignature(snapshot, prepareVerify(options), key, encodeJsonUnencodedPayload); + return result; +} +export { + flattenedVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js new file mode 100644 index 000000000..8f76dd00b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/sign.js @@ -0,0 +1,58 @@ +import { createSignature } from "../../lib/jws_sign.js"; +import { JWSInvalid } from "../../util/errors.js"; +import { assertNotSet, assertUint8Array } from "../../lib/validate.js"; +class IndividualSignature { + #parent; + state; + constructor(sig, key, options) { + this.#parent = sig, this.state = [void 0, void 0, key, options?.crit]; + } + setProtectedHeader(protectedHeader) { + return assertNotSet(this.state[0], "setProtectedHeader"), this.state[0] = protectedHeader, this; + } + setUnprotectedHeader(unprotectedHeader) { + return assertNotSet(this.state[1], "setUnprotectedHeader"), this.state[1] = unprotectedHeader, this; + } + addSignature(...args) { + return this.#parent.addSignature(...args); + } + sign(...args) { + return this.#parent.sign(...args); + } + done() { + return this.#parent; + } +} +class GeneralSign { + #payload; + #signatures = []; + constructor(payload) { + this.#payload = payload; + } + addSignature(key, options) { + const signature = new IndividualSignature(this, key, options); + return this.#signatures.push(signature), signature; + } + async sign() { + if (!this.#signatures.length) + throw new JWSInvalid("at least one signature must be added"); + assertUint8Array(this.#payload, "payload"); + const jws = { + signatures: [], + payload: "" + }, encoded = []; + let b64; + for (const signature of this.#signatures) { + const [protectedHeader, unprotectedHeader, key, crit] = signature.state, [{ payload, ...rest }, signatureB64] = await createSignature([this.#payload, protectedHeader, unprotectedHeader, crit, encoded], key); + if (b64 === void 0) + b64 = signatureB64, jws.payload = payload; + else if (b64 !== signatureB64) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + jws.signatures.push(rest); + } + return jws; + } +} +export { + GeneralSign +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js new file mode 100644 index 000000000..3243408b8 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jws/general/verify.js @@ -0,0 +1,48 @@ +import { encodeJsonUnencodedPayload, parseProtectedHeader, prepareVerify, snapshotJws, verifySignature } from "../../lib/jws_verify.js"; +import { JWSInvalid, JWSSignatureVerificationFailed } from "../../util/errors.js"; +import { isObject } from "../../lib/validate.js"; +function snapshotSignature(signature, payload) { + try { + const jws = snapshotJws(signature, [payload]), protectedHeader = parseProtectedHeader(jws.protected), { b64, crit } = protectedHeader; + return [ + jws, + protectedHeader, + Array.isArray(crit) && crit.includes("b64") ? typeof b64 == "boolean" ? b64 ? 1 : 2 : 0 : 1 + ]; + } catch { + return; + } +} +async function generalVerify(jws, key, options) { + if (!isObject(jws)) + throw new JWSInvalid("General JWS must be an object"); + const { signatures, payload: inputPayload } = jws; + if (!Array.isArray(signatures)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + const signatureEntries = Array.from(signatures); + if (!signatureEntries.every(isObject)) + throw new JWSInvalid("JWS Signatures missing or incorrect type"); + let shared; + try { + if (inputPayload === void 0) + throw new Error(); + shared = prepareVerify(options); + } catch { + throw new JWSSignatureVerificationFailed(); + } + const payload = inputPayload instanceof Uint8Array ? new Uint8Array(inputPayload) : inputPayload, candidates = signatureEntries.map((signature) => snapshotSignature(signature, payload)).filter((candidate) => candidate !== void 0); + let modes = 0; + for (const [, , mode] of candidates) + if (modes |= mode, modes === 3) + throw new JWSInvalid("inconsistent use of JWS Unencoded Payload (RFC7797)"); + for (const candidate of candidates) + try { + const [result] = await verifySignature(candidate[0], shared, key, encodeJsonUnencodedPayload, candidate[1]); + return result; + } catch { + } + throw new JWSSignatureVerificationFailed(); +} +export { + generalVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js new file mode 100644 index 000000000..cfbe4730c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/decrypt.js @@ -0,0 +1,13 @@ +import { prepareDecrypt, decryptCompact } from "../lib/jwe_decrypt.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTClaimValidationFailed } from "../util/errors.js"; +async function jwtDecrypt(jwt, key, options) { + const { plaintext, ...result } = await decryptCompact(jwt, prepareDecrypt(options), key), { protectedHeader } = result, payload = validateClaimsSet(protectedHeader, plaintext, options); + for (const claim of ["iss", "sub", "aud"]) + if (protectedHeader[claim] !== void 0 && (claim === "aud" ? JSON.stringify(protectedHeader.aud) !== JSON.stringify(payload.aud) : protectedHeader[claim] !== payload[claim])) + throw new JWTClaimValidationFailed(`replicated "${claim}" claim header parameter mismatch`, payload, claim, "mismatch"); + return { payload, ...result }; +} +export { + jwtDecrypt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js new file mode 100644 index 000000000..c26908857 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/encrypt.js @@ -0,0 +1,45 @@ +import { compactJWE, createJWE } from "../lib/jwe_encrypt.js"; +import { JWTClaimsBuilder, jwtClaim, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const EncryptJWT_base = JWTClaimsBuilder; +class EncryptJWT extends EncryptJWT_base { + #input = [void 0]; + #replicateIssuerAsHeader; + #replicateSubjectAsHeader; + #replicateAudienceAsHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#input[1], "setProtectedHeader"), this.#input[1] = protectedHeader, this; + } + setKeyManagementParameters(parameters) { + return assertNotSet(this.#input[7], "setKeyManagementParameters"), this.#input[7] = parameters, this; + } + setContentEncryptionKey(cek) { + return assertNotSet(this.#input[5], "setContentEncryptionKey"), this.#input[5] = cek, this; + } + setInitializationVector(iv) { + return assertNotSet(this.#input[6], "setInitializationVector"), this.#input[6] = iv, this; + } + replicateIssuerAsHeader() { + return this.#replicateIssuerAsHeader = !0, this; + } + replicateSubjectAsHeader() { + return this.#replicateSubjectAsHeader = !0, this; + } + replicateAudienceAsHeader() { + return this.#replicateAudienceAsHeader = !0, this; + } + async encrypt(key, options) { + const plaintext = jwtData(this); + this.#input[1] && (this.#replicateIssuerAsHeader || this.#replicateSubjectAsHeader || this.#replicateAudienceAsHeader) && (this.#input[1] = { + ...this.#input[1], + iss: this.#replicateIssuerAsHeader ? jwtClaim(this, "iss") : void 0, + sub: this.#replicateSubjectAsHeader ? jwtClaim(this, "sub") : void 0, + aud: this.#replicateAudienceAsHeader ? jwtClaim(this, "aud") : void 0 + }); + const input = [...this.#input]; + return input[0] = plaintext, compactJWE(await createJWE(input, key, options)); + } +} +export { + EncryptJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js new file mode 100644 index 000000000..4d5e51c99 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/sign.js @@ -0,0 +1,19 @@ +import { createCompactSignature } from "../lib/jws_sign.js"; +import { JWTInvalid } from "../util/errors.js"; +import { JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +import { assertNotSet } from "../lib/validate.js"; +const SignJWT_base = JWTClaimsBuilder; +class SignJWT extends SignJWT_base { + #protectedHeader; + setProtectedHeader(protectedHeader) { + return assertNotSet(this.#protectedHeader, "setProtectedHeader"), this.#protectedHeader = protectedHeader, this; + } + async sign(key, options) { + return createCompactSignature(jwtData(this), this.#protectedHeader, options?.crit, key, () => { + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + }); + } +} +export { + SignJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js new file mode 100644 index 000000000..0f01cc7a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/unsecured.js @@ -0,0 +1,34 @@ +import * as b64u from "../util/base64url.js"; +import { decodeBase64url, parseJoseHeader, JWS_RECOGNIZED, validateB64, validateCrit } from "../lib/validate.js"; +import { JWSInvalid, JWTInvalid } from "../util/errors.js"; +import { validateClaimsSet, JWTClaimsBuilder, jwtData } from "../lib/jwt_claims_set.js"; +const UnsecuredJWT_base = JWTClaimsBuilder; +class UnsecuredJWT extends UnsecuredJWT_base { + encode() { + const header = b64u.encode(JSON.stringify({ alg: "none" })), payload = b64u.encode(jwtData(this)); + return `${header}.${payload}.`; + } + static decode(jwt, options) { + if (typeof jwt != "string") + throw new JWTInvalid("Unsecured JWT must be a string"); + const { 0: encodedHeader, 1: encodedPayload, 2: signature, length } = jwt.split("."); + if (length !== 3 || signature !== "") + throw new JWTInvalid("Invalid Unsecured JWT"); + let header, b64; + try { + header = parseJoseHeader(encodedHeader, JWSInvalid, "JWS Protected Header is invalid"); + const extensions = validateCrit(JWSInvalid, JWS_RECOGNIZED, void 0, header, header); + b64 = validateB64(header, extensions); + } catch (cause) { + throw cause instanceof JWSInvalid ? new JWTInvalid("Invalid Unsecured JWT", { cause }) : cause; + } + if (header.alg !== "none") + throw new JWTInvalid("Invalid Unsecured JWT"); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + return { payload: validateClaimsSet(header, decodeBase64url(encodedPayload, "payload", JWTInvalid), options), header }; + } +} +export { + UnsecuredJWT +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js new file mode 100644 index 000000000..8e02f3a8d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/jwt/verify.js @@ -0,0 +1,13 @@ +import { prepareVerify, verifyCompact } from "../lib/jws_verify.js"; +import { validateClaimsSet } from "../lib/jwt_claims_set.js"; +import { JWTInvalid } from "../util/errors.js"; +async function jwtVerify(jwt, key, options) { + const [verified, b64] = await verifyCompact(jwt, prepareVerify(options), key); + if (!b64) + throw new JWTInvalid("JWTs MUST NOT use unencoded payload"); + const payload = validateClaimsSet(verified.protectedHeader, verified.payload, options); + return { ...verified, payload }; +} +export { + jwtVerify +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js new file mode 100644 index 000000000..49016c5a6 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/export.js @@ -0,0 +1,35 @@ +import { toSPKI as exportPublic, toPKCS8 as exportPrivate } from "../lib/asn1.js"; +import { invalidKeyInput, isCryptoKey, isKeyObject } from "../lib/key.js"; +import { encode as b64u } from "../util/base64url.js"; +function exportSPKI(key) { + return exportPublic(key); +} +function exportPKCS8(key) { + return exportPrivate(key); +} +async function exportJWK(key) { + if (isKeyObject(key)) + if (key.type === "secret") + key = key.export(); + else + return key.export({ format: "jwk" }); + if (key instanceof Uint8Array) + return { + kty: "oct", + k: b64u(key) + }; + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "Uint8Array")); + if (!key.extractable) + throw new TypeError("non-extractable CryptoKey cannot be exported as a JWK"); + const jwk = await crypto.subtle.exportKey("jwk", key); + delete jwk.ext, delete jwk.key_ops, delete jwk.use, jwk.kty !== "AKP" && delete jwk.alg; + for (const parameter of Object.keys(jwk)) + jwk[parameter] === void 0 && delete jwk[parameter]; + return jwk; +} +export { + exportJWK, + exportPKCS8, + exportSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js new file mode 100644 index 000000000..c687f420d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_key_pair.js @@ -0,0 +1,35 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { validateExtractableOption } from "../lib/key.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +function getModulusLengthOption(options) { + const modulusLength = options?.modulusLength ?? 2048; + if (typeof modulusLength != "number" || !Number.isInteger(modulusLength) || modulusLength < 2048) + throw new JOSENotSupported("Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used"); + return modulusLength; +} +async function generateKeyPair(alg, options) { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + let algorithm; + if (entry.resolve) { + const crv = options?.crv ?? "P-256"; + if (!["P-256", "P-384", "P-521", "X25519"].includes(crv)) + throw new JOSENotSupported("Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, and X25519"); + algorithm = entry.resolve({ crv }); + } else { + if (entry.crv !== void 0 && options?.crv !== void 0 && options.crv !== entry.crv) + throw new JOSENotSupported(`Invalid or unsupported crv option provided, the only supported value for ${alg} is ${entry.crv}`); + algorithm = entry.kty[0] === "RSA" ? { + ...entry.subtle, + publicExponent: Uint8Array.of(1, 0, 1), + modulusLength: getModulusLengthOption(options) + } : entry.subtle; + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, [ + ...entry.usages[1], + ...entry.usages[0] + ]); +} +export { + generateKeyPair +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js new file mode 100644 index 000000000..b085b4919 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/generate_secret.js @@ -0,0 +1,36 @@ +import { unsupportedAlg, algArgument } from "../lib/key_algorithm.js"; +import { validateExtractableOption } from "../lib/key.js"; +async function generateSecret(alg, options) { + const extractable = validateExtractableOption(options?.extractable); + let length, algorithm, keyUsages; + switch (alg) { + case "HS256": + case "HS384": + case "HS512": + length = +alg.slice(-3), algorithm = { name: "HMAC", hash: `SHA-${length}`, length }, keyUsages = ["sign", "verify"]; + break; + case "A128CBC-HS256": + case "A192CBC-HS384": + case "A256CBC-HS512": + return crypto.getRandomValues(new Uint8Array(+alg.slice(-3) >> 3)); + case "A128KW": + case "A192KW": + case "A256KW": + length = +alg.slice(1, 4), algorithm = { name: "AES-KW", length }, keyUsages = ["wrapKey", "unwrapKey"]; + break; + case "A128GCMKW": + case "A192GCMKW": + case "A256GCMKW": + case "A128GCM": + case "A192GCM": + case "A256GCM": + length = +alg.slice(1, 4), algorithm = { name: "AES-GCM", length }, keyUsages = ["encrypt", "decrypt"]; + break; + default: + unsupportedAlg(algArgument); + } + return crypto.subtle.generateKey(algorithm, extractable ?? !1, keyUsages); +} +export { + generateSecret +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js new file mode 100644 index 000000000..b171ae5de --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/key/import.js @@ -0,0 +1,53 @@ +import { decode as decodeBase64URL } from "../util/base64url.js"; +import { fromSPKI, fromPKCS8, fromX509 } from "../lib/asn1.js"; +import { jwkToKey, validateExtractableOption, normalizeJwk } from "../lib/key.js"; +import { keyAlgorithm } from "../lib/key_algorithm.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { isObject } from "../lib/validate.js"; +async function importSPKI(spki, alg, options) { + if (typeof spki != "string" || spki.indexOf("-----BEGIN PUBLIC KEY-----") !== 0) + throw new TypeError('"spki" must be SPKI formatted string'); + return fromSPKI(spki, alg, options); +} +async function importX509(x509, alg, options) { + if (typeof x509 != "string" || x509.indexOf("-----BEGIN CERTIFICATE-----") !== 0) + throw new TypeError('"x509" must be X.509 formatted string'); + return fromX509(x509, alg, options); +} +async function importPKCS8(pkcs8, alg, options) { + if (typeof pkcs8 != "string" || pkcs8.indexOf("-----BEGIN PRIVATE KEY-----") !== 0) + throw new TypeError('"pkcs8" must be PKCS#8 formatted string'); + return fromPKCS8(pkcs8, alg, options); +} +async function importJWK(jwk, alg, options) { + if (!isObject(jwk)) + throw new TypeError("JWK must be an object"); + const normalized = normalizeJwk(jwk), extractable = validateExtractableOption(options?.extractable), { alg: jwkAlg } = normalized; + if (alg ??= jwkAlg, normalized.kty !== "oct" && !alg) + throw new TypeError('"alg" argument is required when "jwk.alg" is not present'); + switch (normalized.kty) { + case "oct": + if (typeof normalized.k != "string") + throw new TypeError('missing "k" (Key Value) Parameter value'); + return decodeBase64URL(normalized.k); + case "AKP": { + if (typeof jwkAlg != "string" || !jwkAlg) + throw new TypeError('missing "alg" (Algorithm) Parameter value'); + if (alg !== jwkAlg) + throw new TypeError("JWK alg and alg option value mismatch"); + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + } + case "RSA": + case "EC": + case "OKP": + return jwkToKey(keyAlgorithm(alg), normalized, extractable); + default: + throw new JOSENotSupported('Unsupported "kty" (Key Type) Parameter value'); + } +} +export { + importJWK, + importPKCS8, + importSPKI, + importX509 +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js new file mode 100644 index 000000000..51f781b9f --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/asn1.js @@ -0,0 +1,138 @@ +import { invalidKeyInput, isCryptoKey, isKeyObject, validateExtractableOption } from "./key.js"; +import { encodeBase64, decodeBase64 } from "./buffer_utils.js"; +import { JOSENotSupported } from "../util/errors.js"; +import { keyAlgorithm, unsupportedAlg, algArgument } from "./key_algorithm.js"; +const formatPEM = (b64, descriptor) => { + const newlined = (b64.match(/.{1,64}/g) || []).join(` +`); + return `-----BEGIN ${descriptor}----- +${newlined} +-----END ${descriptor}-----`; +}, genericExport = async (keyType, keyFormat, key) => { + if (isKeyObject(key)) { + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return key.export({ format: "pem", type: keyFormat }); + } + if (!isCryptoKey(key)) + throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject")); + if (!key.extractable) + throw new TypeError("CryptoKey is not extractable"); + if (key.type !== keyType) + throw new TypeError(`key is not a ${keyType} key`); + return formatPEM(encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))), `${keyType.toUpperCase()} KEY`); +}, toSPKI = (key) => genericExport("public", "spki", key), toPKCS8 = (key) => genericExport("private", "pkcs8", key), bytesEqual = (a, b) => { + if (a.byteLength !== b.length) + return !1; + for (let i = 0; i < a.byteLength; i++) + if (a[i] !== b[i]) + return !1; + return !0; +}, createASN1State = (data) => ({ data, pos: 0 }), readByte = (state) => { + const byte = state.data[state.pos++]; + if (byte === void 0) + throw new Error("Unexpected end of ASN.1 input"); + return byte; +}, parseLength = (state) => { + const first = readByte(state); + if (first & 128) { + const lengthOfLen = first & 127; + let length = 0; + for (let i = 0; i < lengthOfLen; i++) + length = length << 8 | readByte(state); + return length; + } + return first; +}, skipElement = (state, count = 1) => { + for (; count-- > 0; ) { + state.pos++; + const length = parseLength(state); + state.pos += length; + } +}, expectTag = (state, expectedTag, errorMessage) => { + if (readByte(state) !== expectedTag) + throw new Error(errorMessage); +}, getSubarray = (state, length) => { + if (length < 0 || state.pos + length > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + const result = state.data.subarray(state.pos, state.pos + length); + return state.pos += length, result; +}, parseAlgorithmOID = (state) => { + expectTag(state, 6, "Expected algorithm OID"); + const oidLen = parseLength(state); + return getSubarray(state, oidLen); +}; +function parseKeyHeader(state, keyFormat) { + if (expectTag(state, 48, `Invalid ${keyFormat === "spki" ? "SPKI" : "PKCS#8"} structure`), parseLength(state), keyFormat === "pkcs8") { + expectTag(state, 2, "Expected version field"); + const length = parseLength(state); + state.pos += length; + } + expectTag(state, 48, "Expected algorithm identifier"), parseLength(state); +} +const parseECAlgorithmIdentifier = (state) => { + const algOid = parseAlgorithmOID(state); + if (bytesEqual(algOid, [43, 101, 110])) + return "X25519"; + if (!bytesEqual(algOid, [42, 134, 72, 206, 61, 2, 1])) + throw new Error("Unsupported key algorithm"); + expectTag(state, 6, "Expected curve OID"); + const curveOidLen = parseLength(state), curveOid = getSubarray(state, curveOidLen); + if (bytesEqual(curveOid, [42, 134, 72, 206, 61, 3, 1, 7])) + return "P-256"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 34])) + return "P-384"; + if (bytesEqual(curveOid, [43, 129, 4, 0, 35])) + return "P-521"; + throw new Error("Unsupported named curve"); +}, genericImport = async (keyFormat, keyData, alg, options) => { + const extractable = validateExtractableOption(options?.extractable), entry = keyAlgorithm(alg, algArgument); + entry.secret && unsupportedAlg(algArgument); + const isPublic = keyFormat === "spki"; + let algorithm; + if (entry.resolve) + try { + const state = createASN1State(keyData); + parseKeyHeader(state, keyFormat), algorithm = entry.resolve({ crv: parseECAlgorithmIdentifier(state) }); + } catch { + throw new JOSENotSupported("Invalid or unsupported key format"); + } + else + algorithm = entry.subtle; + return crypto.subtle.importKey(keyFormat, keyData, algorithm, extractable ?? isPublic, entry.usages[isPublic ? 0 : 1]); +}, processPEMData = (pem, pattern) => decodeBase64(pem.replace(pattern, "")), fromPKCS8 = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g); + return genericImport("pkcs8", keyData, alg, options); +}, fromSPKI = (pem, alg, options) => { + const keyData = processPEMData(pem, /(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g); + return genericImport("spki", keyData, alg, options); +}; +function spkiFromX509(buf) { + const state = createASN1State(buf); + expectTag(state, 48, "Invalid certificate structure"); + const certificateLength = parseLength(state); + if (certificateLength < 0 || state.pos + certificateLength > state.data.length) + throw new Error("Unexpected end of ASN.1 input"); + expectTag(state, 48, "Invalid tbsCertificate structure"), parseLength(state), buf[state.pos] === 160 ? skipElement(state, 6) : skipElement(state, 5); + const spkiStart = state.pos; + expectTag(state, 48, "Invalid SPKI structure"); + const spkiContentLen = parseLength(state); + return buf.subarray(spkiStart, spkiStart + spkiContentLen + (state.pos - spkiStart)); +} +const fromX509 = (pem, alg, options) => { + let spki; + try { + const certificate = processPEMData(pem, /(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g); + spki = spkiFromX509(certificate); + } catch (cause) { + throw new TypeError("Failed to parse the X.509 certificate", { cause }); + } + return genericImport("spki", spki, alg, options); +}; +export { + fromPKCS8, + fromSPKI, + fromX509, + toPKCS8, + toSPKI +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js new file mode 100644 index 000000000..b3cad85a9 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/buffer_utils.js @@ -0,0 +1,75 @@ +const encoder = new TextEncoder(), decoder = new TextDecoder(), strictDecoder = new TextDecoder("utf-8", { fatal: !0 }), MAX_INT32 = 2 ** 32; +function concat(...buffers) { + const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size); + let i = 0; + for (const buffer of buffers) + buf.set(buffer, i), i += buffer.length; + return buf; +} +function writeUInt32BE(buf, value, offset) { + if (value < 0 || value >= MAX_INT32) + throw new RangeError(`value must be >= 0 and <= ${MAX_INT32 - 1}. Received ${value}`); + buf.set([value >>> 24, value >>> 16, value >>> 8, value & 255], offset); +} +function uint64be(value) { + const high = Math.floor(value / MAX_INT32), low = value % MAX_INT32, buf = new Uint8Array(8); + return writeUInt32BE(buf, high, 0), writeUInt32BE(buf, low, 4), buf; +} +function uint32be(value) { + const buf = new Uint8Array(4); + return writeUInt32BE(buf, value), buf; +} +const NON_ASCII = /[^\x00-\x7f]/; +function encode(string) { + if (typeof string == "string" && string.length >= 128) { + if (NON_ASCII.test(string)) + throw new TypeError("non-ASCII string encountered in encode()"); + return encoder.encode(string); + } + const bytes = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + const code = string.charCodeAt(i); + if (code > 127) + throw new TypeError("non-ASCII string encountered in encode()"); + bytes[i] = code; + } + return bytes; +} +function encodeBase64(input, url = !1) { + if (Uint8Array.prototype.toBase64) + return input.toBase64({ alphabet: url ? "base64url" : "base64", omitPadding: url }); + const CHUNK_SIZE = 32768, arr = []; + for (let i = 0; i < input.length; i += CHUNK_SIZE) + arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE))); + const encoded = btoa(arr.join("")); + return url ? encoded.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_") : encoded; +} +function decodeBase64(encoded, url = !1) { + if (Uint8Array.fromBase64) + return Uint8Array.fromBase64(encoded, { alphabet: url ? "base64url" : "base64" }); + if (url) { + if (encoded.includes("+") || encoded.includes("/")) + throw new TypeError("Invalid base64url"); + encoded = encoded.replace(/-/g, "+").replace(/_/g, "/"); + } + const binary = atob(encoded), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return bytes; +} +async function digest(algorithm, data) { + const subtleDigest = `SHA-${algorithm.slice(-3)}`; + return new Uint8Array(await crypto.subtle.digest(subtleDigest, data)); +} +export { + concat, + decodeBase64, + decoder, + digest, + encode, + encodeBase64, + encoder, + strictDecoder, + uint32be, + uint64be +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js new file mode 100644 index 000000000..1ea355e91 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/content_encryption.js @@ -0,0 +1,90 @@ +import { concat, uint64be } from "./buffer_utils.js"; +import { rawKey, invalidKeyInput, isCryptoKey } from "./key.js"; +import { JWEDecryptionFailed, JWEInvalid } from "../util/errors.js"; +const generateCek = (enc) => crypto.getRandomValues(new Uint8Array(enc.cekBits >> 3)); +function checkCekLength(cek, expected) { + const actual = cek.byteLength << 3; + if (actual !== expected) + throw new JWEInvalid(`Invalid Content Encryption Key length. Expected ${expected} bits, got ${actual} bits`); +} +const generateIv = (enc) => crypto.getRandomValues(new Uint8Array(enc.ivBits >> 3)); +function checkIvLength(enc, iv) { + if (iv.length << 3 !== enc.ivBits) + throw new JWEInvalid("Invalid Initialization Vector length"); +} +async function cbcKeySetup(enc, cek, usage) { + if (!(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "Uint8Array")); + const keySize = enc.cekBits >> 1, encKey = await crypto.subtle.importKey("raw", cek.subarray(keySize >> 3), "AES-CBC", !1, [usage]), macKey = await crypto.subtle.importKey("raw", cek.subarray(0, keySize >> 3), { + hash: `SHA-${keySize << 1}`, + name: "HMAC" + }, !1, ["sign"]); + return [encKey, macKey, keySize]; +} +async function cbcHmacTag(macKey, macData, keySize) { + return new Uint8Array((await crypto.subtle.sign("HMAC", macKey, macData)).slice(0, keySize >> 3)); +} +async function cbcEncrypt(enc, plaintext, cek, iv, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "encrypt"), ciphertext = new Uint8Array(await crypto.subtle.encrypt({ + iv, + name: "AES-CBC" + }, encKey, plaintext)), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), tag = await cbcHmacTag(macKey, macData, keySize); + return { ciphertext, tag, iv }; +} +async function timingSafeEqual(a, b) { + const algorithm = { name: "HMAC", hash: "SHA-256" }, key = await crypto.subtle.generateKey(algorithm, !1, ["sign", "verify"]), aHmac = await crypto.subtle.sign(algorithm, key, a); + return crypto.subtle.verify(algorithm, key, aHmac, b); +} +async function cbcDecrypt(enc, cek, ciphertext, iv, tag, aad) { + const [encKey, macKey, keySize] = await cbcKeySetup(enc, cek, "decrypt"), macData = concat(aad, iv, ciphertext, uint64be(aad.length * 8)), expectedTag = await cbcHmacTag(macKey, macData, keySize); + try { + if (await timingSafeEqual(tag, expectedTag)) + return new Uint8Array(await crypto.subtle.decrypt({ iv, name: "AES-CBC" }, encKey, ciphertext)); + } catch { + } + throw new JWEDecryptionFailed(); +} +async function encrypt(enc, plaintext, cek, iv, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (iv ? checkIvLength(enc, iv) : iv = generateIv(enc), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcEncrypt(enc, plaintext, cek, iv, aad); + const encKey = await rawKey(cek, enc.subtle, "encrypt"), encrypted = new Uint8Array(await crypto.subtle.encrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, plaintext)); + return { ciphertext: encrypted.subarray(0, -16), tag: encrypted.subarray(-16), iv }; +} +async function decrypt(enc, cek, ciphertext, iv, tag, aad) { + if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) + throw new TypeError(invalidKeyInput(cek, "CryptoKey", "KeyObject", "Uint8Array", "JSON Web Key")); + if (!iv) + throw new JWEInvalid("JWE Initialization Vector missing"); + if (!tag) + throw new JWEInvalid("JWE Authentication Tag missing"); + if (!enc.cbc && tag.length !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + if (checkIvLength(enc, iv), cek instanceof Uint8Array && checkCekLength(cek, enc.cekBits), enc.cbc) + return cbcDecrypt(enc, cek, ciphertext, iv, tag, aad); + const encKey = await rawKey(cek, enc.subtle, "decrypt"); + try { + return new Uint8Array(await crypto.subtle.decrypt({ + additionalData: aad, + iv, + name: "AES-GCM", + tagLength: 128 + }, encKey, concat(ciphertext, tag))); + } catch { + throw new JWEDecryptionFailed(); + } +} +export { + checkCekLength, + checkIvLength, + decrypt, + encrypt, + generateCek, + generateIv +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js new file mode 100644 index 000000000..27ffba278 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/deflate.js @@ -0,0 +1,40 @@ +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { concat } from "./buffer_utils.js"; +function validateZip(joseHeader, protectedHeader) { + if (joseHeader.zip !== void 0 && joseHeader.zip !== "DEF") + throw new JOSENotSupported('Unsupported JWE "zip" (Compression Algorithm) Header Parameter value.'); + if (joseHeader.zip !== void 0 && !protectedHeader?.zip) + throw new JWEInvalid('JWE "zip" (Compression Algorithm) Header Parameter MUST be in a protected header.'); +} +function supported(name) { + if (typeof globalThis[name] > "u") + throw new JOSENotSupported(`JWE "zip" (Compression Algorithm) Header Parameter requires the ${name} API.`); +} +async function transform(stream, input, maxLength = 1 / 0) { + const writer = stream.writable.getWriter(); + writer.write(input).catch(() => { + }), writer.close().catch(() => { + }); + const chunks = []; + let length = 0; + const reader = stream.readable.getReader(); + for (; ; ) { + const { value, done } = await reader.read(); + if (done) + break; + if (chunks.push(value), length += value.byteLength, maxLength !== 1 / 0 && length > maxLength) + throw new JWEInvalid("Decompressed plaintext exceeded the configured limit"); + } + return concat(...chunks); +} +async function compress(input) { + return supported("CompressionStream"), transform(new CompressionStream("deflate-raw"), input); +} +async function decompress(input, maxLength) { + return supported("DecompressionStream"), transform(new DecompressionStream("deflate-raw"), input, maxLength); +} +export { + compress, + decompress, + validateZip +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js new file mode 100644 index 000000000..a87af4eb5 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_algorithms.js @@ -0,0 +1,116 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const wrap = [ + ["encrypt", "wrapKey"], + ["decrypt", "unwrapKey"] +], derive = [[], ["deriveBits"]], none = [[], []]; +function rsaes(bits) { + return { + kty: ["RSA"], + mode: "key-encryption", + subtle: { name: "RSA-OAEP", hash: `SHA-${bits}` }, + usages: wrap, + ops: ["wrapKey", "unwrapKey"] + }; +} +function ecdh(mode) { + return { + kty: ["EC", "OKP"], + mode, + subtle: { name: "ECDH" }, + resolve: ({ kty, crv, asymmetricKeyType }) => { + if (crv === "X25519" || asymmetricKeyType === "x25519") + return { name: "X25519" }; + if (kty === "OKP") + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + return { name: "ECDH", namedCurve: crv }; + }, + usages: derive, + ops: [void 0, "deriveBits"] + }; +} +function aeskw(bits, gcm = !1) { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: gcm ? "AES-GCM" : "AES-KW", length: bits }, + usages: none, + ops: gcm ? ["encrypt", "decrypt"] : ["wrapKey", "unwrapKey"] + }; +} +function pbes2() { + return { + kty: ["oct"], + mode: "key-wrapping", + secret: !0, + subtle: { name: "PBKDF2" }, + usages: none, + ops: ["deriveBits", "deriveBits"] + }; +} +const JWE = table({ + dir: { + kty: ["oct"], + mode: "direct-encryption", + secret: !0, + subtle: { name: "AES-GCM" }, + usages: none, + ops: ["encrypt", "decrypt"] + }, + "RSA-OAEP": rsaes(1), + "RSA-OAEP-256": rsaes(256), + "RSA-OAEP-384": rsaes(384), + "RSA-OAEP-512": rsaes(512), + "ECDH-ES": ecdh("direct-key-agreement"), + "ECDH-ES+A128KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A192KW": ecdh("key-agreement-with-key-wrapping"), + "ECDH-ES+A256KW": ecdh("key-agreement-with-key-wrapping"), + A128KW: aeskw(128), + A192KW: aeskw(192), + A256KW: aeskw(256), + A128GCMKW: aeskw(128, !0), + A192GCMKW: aeskw(192, !0), + A256GCMKW: aeskw(256, !0), + "PBES2-HS256+A128KW": pbes2(), + "PBES2-HS384+A192KW": pbes2(), + "PBES2-HS512+A256KW": pbes2() +}), contentOps = ["encrypt", "decrypt"]; +function contentEncryption(bits, cbc = !1) { + return { + kty: ["oct"], + secret: !0, + subtle: { name: cbc ? "AES-CBC" : "AES-GCM", length: bits }, + usages: none, + ops: contentOps, + cekBits: bits, + ivBits: cbc ? 128 : 96, + cbc + }; +} +const ENC = table({ + A128GCM: contentEncryption(128), + A192GCM: contentEncryption(192), + A256GCM: contentEncryption(256), + "A128CBC-HS256": contentEncryption(256, !0), + "A192CBC-HS384": contentEncryption(384, !0), + "A256CBC-HS512": contentEncryption(512, !0) +}); +function unsupported(parameter, name) { + throw new JOSENotSupported(`Invalid or unsupported "${parameter}" (JWE ${name}) header value`); +} +function jweAlgorithm(alg) { + return (typeof alg == "string" ? JWE[alg] : void 0) ?? unsupported("alg", "Algorithm"); +} +function isJWECEKTransport(algorithm) { + return algorithm.mode === "key-wrapping" || algorithm.mode === "key-encryption" || algorithm.mode === "key-agreement-with-key-wrapping"; +} +function jweEncryption(enc) { + return (typeof enc == "string" ? ENC[enc] : void 0) ?? unsupported("enc", "Encryption Algorithm"); +} +export { + JWE, + isJWECEKTransport, + jweAlgorithm, + jweEncryption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js new file mode 100644 index 000000000..e5453b9ba --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_decrypt.js @@ -0,0 +1,187 @@ +import { decrypt, generateCek } from "./content_encryption.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateCrit, validateAlgorithms, JWE_RECOGNIZED } from "./validate.js"; +import { JOSEAlgNotAllowed, JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decryptKeyManagement } from "./key_management.js"; +import { decoder } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, isJWECEKTransport, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { decompress, validateZip } from "./deflate.js"; +function snapshotSharedJWE(jwe) { + const { aad, ciphertext, iv, protected: encodedProtected, tag, unprotected } = jwe; + if (iv !== void 0 && (typeof iv != "string" || !iv)) + throw new JWEInvalid("JWE Initialization Vector incorrect type"); + if (typeof ciphertext != "string") + throw new JWEInvalid("JWE Ciphertext missing or incorrect type"); + if (tag !== void 0 && (typeof tag != "string" || !tag)) + throw new JWEInvalid("JWE Authentication Tag incorrect type"); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWEInvalid("JWE Protected Header incorrect type"); + if (aad !== void 0 && (typeof aad != "string" || !aad)) + throw new JWEInvalid("JWE AAD incorrect type"); + if (unprotected !== void 0 && !isObject(unprotected)) + throw new JWEInvalid("JWE Shared Unprotected Header incorrect type"); + return { + aad, + ciphertext, + iv, + protected: encodedProtected, + tag, + unprotected: unprotected === void 0 ? void 0 : { ...unprotected } + }; +} +function snapshotRecipientJWE(recipient) { + let header, headerAlg; + try { + const { header: inputHeader } = recipient; + if (isObject(inputHeader)) { + headerAlg = inputHeader.alg; + const parameters = Object.keys(inputHeader); + parameters.includes("alg") || (headerAlg = void 0), header = Object.fromEntries(parameters.map((parameter) => [ + parameter, + parameter === "alg" ? headerAlg : inputHeader[parameter] + ])); + } else + header = inputHeader; + const { encrypted_key: encryptedKey } = recipient; + return [{ encrypted_key: encryptedKey, header }, headerAlg]; + } catch (error) { + return [void 0, headerAlg, error]; + } +} +function checkRecipient(jwe) { + const { encrypted_key: encryptedKey, header } = jwe; + if (encryptedKey !== void 0 && typeof encryptedKey != "string") + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWEInvalid("JWE Per-Recipient Unprotected Header incorrect type"); + if (jwe.protected === void 0 && header === void 0 && jwe.unprotected === void 0) + throw new JWEInvalid("JOSE Header missing"); +} +function shareJWE(jwe) { + const { protected: encodedProtected, ciphertext, iv, tag, aad } = jwe; + let parsedProt; + return encodedProtected !== void 0 && (parsedProt = parseJoseHeader(encodedProtected, JWEInvalid, "JWE Protected Header is invalid")), [ + parsedProt, + decodeBase64url(ciphertext, "ciphertext", JWEInvalid), + iv !== void 0 ? decodeBase64url(iv, "iv", JWEInvalid) : void 0, + tag !== void 0 ? decodeBase64url(tag, "tag", JWEInvalid) : void 0, + encodeBase64url((encodedProtected ?? "") + (aad !== void 0 ? `.${aad}` : ""), "aad", JWEInvalid) + ]; +} +function prepareDecrypt(options) { + return [ + options && validateAlgorithms("keyManagementAlgorithms", options.keyManagementAlgorithms), + options && validateAlgorithms("contentEncryptionAlgorithms", options.contentEncryptionAlgorithms), + options?.crit, + options?.maxPBES2Count, + options?.maxDecompressedLength + ]; +} +async function decryptJWE(jwe, shared, key, token = shareJWE(jwe)) { + const [parsedProt, ciphertext, iv, tag, additionalData] = token, { header, unprotected, aad } = jwe; + let joseHeader; + if (header !== void 0 || unprotected !== void 0) { + if (!isDisjoint(parsedProt, header, unprotected)) + throw new JWEInvalid("JWE Protected, JWE Unprotected Header, and JWE Per-Recipient Unprotected Header Parameter names must be disjoint"); + joseHeader = { ...parsedProt, ...header, ...unprotected }; + } else + joseHeader = parsedProt ?? {}; + const [keyManagementAlgorithms, contentEncryptionAlgorithms, crit, maxPBES2Count, maxDecompressedLength] = shared, { encrypted_key: encodedKey } = jwe; + validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, parsedProt, joseHeader), validateZip(joseHeader, parsedProt); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid("missing JWE Algorithm (alg) in JWE Header"); + const selected = JWE[alg]; + if (encodedKey === "" && (!selected || !isJWECEKTransport(selected))) + throw new JWEInvalid("JWE Encrypted Key incorrect type"); + const integrated = selected?.mode === "integrated-encryption"; + if (!integrated && (typeof enc != "string" || !enc)) + throw new JWEInvalid("missing JWE Encryption Algorithm (enc) in JWE Header"); + if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg) || !keyManagementAlgorithms && alg.startsWith("PBES2")) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + let encEntry; + if (integrated) { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (iv?.byteLength) + throw new JWEInvalid("JWE Initialization Vector must be empty for integrated encryption"); + if (tag?.byteLength) + throw new JWEInvalid("JWE Authentication Tag must be empty for integrated encryption"); + } else { + if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) + throw new JOSEAlgNotAllowed('"enc" (Encryption Algorithm) Header Parameter value not allowed'); + encEntry = jweEncryption(enc); + } + let encryptedKey; + if (encodedKey !== void 0) + try { + encryptedKey = decodeBase64url(encodedKey, "encrypted_key", JWEInvalid); + } catch (error) { + if (!selected || !isJWECEKTransport(selected)) + throw error; + encryptedKey = new Uint8Array(); + } + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jwe), resolvedKey = !0); + const algEntry = selected ?? jweAlgorithm(alg); + isJWECEKTransport(algEntry) && encryptedKey === void 0 && (encryptedKey = new Uint8Array()); + const k = await prepareKey(algEntry.mode === "direct-encryption" ? encEntry : algEntry, key, "decrypt"); + let plaintext; + if (algEntry.mode === "integrated-encryption") + plaintext = await algEntry.decrypt(k, encryptedKey, ciphertext, additionalData, parsedProt, joseHeader); + else { + const encryption = encEntry; + let cek; + try { + cek = await decryptKeyManagement(algEntry, encryption, k, encryptedKey, joseHeader, maxPBES2Count), isJWECEKTransport(algEntry) && cek instanceof Uint8Array && cek.byteLength << 3 !== encryption.cekBits && (cek = generateCek(encryption)); + } catch (err) { + if (err instanceof TypeError || err instanceof JWEInvalid || err instanceof JOSENotSupported) + throw err; + cek = generateCek(encryption); + } + plaintext = await decrypt(encryption, cek, ciphertext, iv, tag, additionalData); + } + if (joseHeader.zip === "DEF") { + const decompressionLimit = maxDecompressedLength ?? 25e4; + if (decompressionLimit === 0) + throw new JOSENotSupported('JWE "zip" (Compression Algorithm) Header Parameter is not supported.'); + if (decompressionLimit !== 1 / 0 && (!Number.isSafeInteger(decompressionLimit) || decompressionLimit < 1)) + throw new TypeError("maxDecompressedLength must be 0, a positive safe integer, or Infinity"); + plaintext = await decompress(plaintext, decompressionLimit).catch((cause) => { + throw cause instanceof JWEInvalid ? cause : new JWEInvalid("Failed to decompress plaintext", { cause }); + }); + } + return { + plaintext, + ...parsedProt && { protectedHeader: parsedProt }, + ...aad !== void 0 && { + additionalAuthenticatedData: decodeBase64url(aad, "aad", JWEInvalid) + }, + ...unprotected && { sharedUnprotectedHeader: unprotected }, + ...header && { unprotectedHeader: header }, + ...resolvedKey && { key: k } + }; +} +async function decryptCompact(jwe, shared, key) { + if (jwe instanceof Uint8Array && (jwe = decoder.decode(jwe)), typeof jwe != "string") + throw new JWEInvalid("Compact JWE must be a string or Uint8Array"); + const { 0: protectedHeader, 1: encryptedKey, 2: iv, 3: ciphertext, 4: tag, length } = jwe.split("."); + if (length !== 5) + throw new JWEInvalid("Invalid Compact JWE"); + return decryptJWE({ + ciphertext, + iv: iv || void 0, + protected: protectedHeader, + tag: tag || void 0, + encrypted_key: encryptedKey || void 0 + }, shared, key); +} +export { + checkRecipient, + decryptCompact, + decryptJWE, + prepareDecrypt, + shareJWE, + snapshotRecipientJWE, + snapshotSharedJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js new file mode 100644 index 000000000..5ab33766b --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwe_encrypt.js @@ -0,0 +1,75 @@ +import { encode as b64u } from "../util/base64url.js"; +import { encrypt } from "./content_encryption.js"; +import { encryptKeyManagement } from "./key_management.js"; +import { JWEInvalid } from "../util/errors.js"; +import { assertUint8Array, isDisjoint, isObject, serializeJoseHeader, validateCrit, validateCritDuplicates, JWE_RECOGNIZED } from "./validate.js"; +import { encode } from "./buffer_utils.js"; +import { prepareKey } from "./key.js"; +import { JWE, jweAlgorithm, jweEncryption } from "./jwe_algorithms.js"; +import { compress, validateZip } from "./deflate.js"; +function checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader) { + if (!isDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)) + throw new JWEInvalid("JWE Protected, JWE Shared Unprotected and JWE Per-Recipient Header Parameter names must be disjoint"); +} +function checkEncryptHeaders(input, options, sharedHeadersNormalized = !1) { + if (!input[1] && !input[2] && !input[3]) + throw new JWEInvalid("either setProtectedHeader, setUnprotectedHeader, or sharedUnprotectedHeader must be called before #encrypt()"); + options !== void 0 && (input[8] = options?.crit); + let [, protectedHeader, unprotectedHeader, sharedUnprotectedHeader, aad, cek, iv, keyManagementParameters, crit] = input; + if (aad !== void 0 && assertUint8Array(aad, "JWE Additional Authenticated Data"), cek !== void 0 && assertUint8Array(cek, "JWE Content Encryption Key"), iv !== void 0 && assertUint8Array(iv, "JWE Initialization Vector"), !sharedHeadersNormalized && protectedHeader !== void 0 && (protectedHeader = serializeJoseHeader(JWEInvalid, protectedHeader)[0], input[1] = protectedHeader), unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWEInvalid, unprotectedHeader)[0], input[2] = unprotectedHeader), !sharedHeadersNormalized && sharedUnprotectedHeader !== void 0 && (sharedUnprotectedHeader = serializeJoseHeader(JWEInvalid, sharedUnprotectedHeader)[0], input[3] = sharedUnprotectedHeader), keyManagementParameters !== void 0 && !isObject(keyManagementParameters)) + throw new TypeError("JWE Key Management Parameters must be an object"); + checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader); + const joseHeader = { + ...protectedHeader, + ...unprotectedHeader, + ...sharedUnprotectedHeader + }; + validateCritDuplicates(JWEInvalid, protectedHeader), validateCrit(JWEInvalid, JWE_RECOGNIZED, crit, protectedHeader, joseHeader), validateZip(joseHeader, protectedHeader); + const { alg, enc } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWEInvalid('JWE "alg" (Algorithm) Header Parameter missing or invalid'); + const algEntry = JWE[alg]; + if (algEntry?.mode === "integrated-encryption") { + if (enc !== void 0) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter must not be present for integrated encryption'); + if (cek !== void 0) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + if (iv !== void 0) + throw new TypeError(`setInitializationVector cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + return [joseHeader, void 0, algEntry]; + } + if (typeof enc != "string" || !enc) + throw new JWEInvalid('JWE "enc" (Encryption Algorithm) Header Parameter missing or invalid'); + return [joseHeader, jweEncryption(enc), algEntry]; +} +async function encryptJWE(input, checked, key) { + const [joseHeader, encEntry, selected] = checked, [inputPlaintext, inputProtectedHeader, inputUnprotectedHeader, sharedUnprotectedHeader, aad, providedCek, inputIv, keyManagementParameters, , unprotectedParameters] = input; + let protectedHeader = inputProtectedHeader, unprotectedHeader = inputUnprotectedHeader; + const algEntry = selected ?? jweAlgorithm(joseHeader.alg); + let encryptedKey, parameters, cek; + algEntry.mode === "integrated-encryption" ? cek = await prepareKey(algEntry, key, "encrypt") : [cek, encryptedKey, parameters] = await encryptKeyManagement(algEntry, encEntry, key, joseHeader, providedCek, keyManagementParameters), parameters && (unprotectedParameters ? unprotectedHeader = unprotectedHeader ? { ...unprotectedHeader, ...parameters } : parameters : protectedHeader = protectedHeader ? { ...protectedHeader, ...parameters } : parameters, checkDisjoint(protectedHeader, unprotectedHeader, sharedUnprotectedHeader)); + const protectedHeaderS = protectedHeader ? b64u(JSON.stringify(protectedHeader)) : "", aadMember = aad?.byteLength ? b64u(aad) : void 0, additionalData = encode(aadMember ? `${protectedHeaderS}.${aadMember}` : protectedHeaderS); + let plaintext = inputPlaintext; + joseHeader.zip === "DEF" && (plaintext = await compress(plaintext).catch((cause) => { + throw new JWEInvalid("Failed to compress plaintext", { cause }); + })); + let ciphertext, tag, iv; + algEntry.mode === "integrated-encryption" ? [encryptedKey, ciphertext] = await algEntry.encrypt(cek, plaintext, additionalData, protectedHeader, joseHeader, keyManagementParameters) : { ciphertext, tag, iv } = await encrypt(encEntry, plaintext, cek, inputIv, additionalData); + const jwe = { + ciphertext: b64u(ciphertext) + }; + return iv && (jwe.iv = b64u(iv)), tag && (jwe.tag = b64u(tag)), encryptedKey?.byteLength && (jwe.encrypted_key = b64u(encryptedKey)), aadMember && (jwe.aad = aadMember), protectedHeader && (jwe.protected = protectedHeaderS), sharedUnprotectedHeader && (jwe.unprotected = sharedUnprotectedHeader), unprotectedHeader && (jwe.header = unprotectedHeader), jwe; +} +async function createJWE(input, key, options) { + return encryptJWE(input, checkEncryptHeaders(input, options), key); +} +function compactJWE(jwe) { + return [jwe.protected, jwe.encrypted_key, jwe.iv, jwe.ciphertext, jwe.tag].join("."); +} +export { + checkDisjoint, + checkEncryptHeaders, + compactJWE, + createJWE, + encryptJWE +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js new file mode 100644 index 000000000..08e2e22ac --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_algorithms.js @@ -0,0 +1,74 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { table } from "./key_descriptor.js"; +const sig = [["verify"], ["sign"]]; +function hmac(bits) { + const subtle = { name: "HMAC", hash: `SHA-${bits}` }; + return { kty: ["oct"], secret: !0, subtle, signing: subtle, usages: sig }; +} +function rsa(bits, saltLength) { + const subtle = { name: saltLength ? "RSA-PSS" : "RSASSA-PKCS1-v1_5", hash: `SHA-${bits}` }; + return { + kty: ["RSA"], + subtle, + signing: saltLength ? { ...subtle, saltLength } : subtle, + usages: sig, + minRsaBits: 2048 + }; +} +function ecdsa(crv, bits) { + return { + kty: ["EC"], + crv, + subtle: { name: "ECDSA", namedCurve: crv }, + signing: { name: "ECDSA", hash: `SHA-${bits}` }, + usages: sig + }; +} +function eddsa() { + const subtle = { name: "Ed25519" }; + return { + kty: ["OKP"], + crv: "Ed25519", + subtle, + signing: subtle, + usages: sig + }; +} +function mldsa(bits) { + const subtle = { name: `ML-DSA-${bits}` }; + return { + kty: ["AKP"], + subtle, + signing: subtle, + usages: sig + }; +} +const JWS = table({ + HS256: hmac(256), + HS384: hmac(384), + HS512: hmac(512), + RS256: rsa(256), + RS384: rsa(384), + RS512: rsa(512), + PS256: rsa(256, 32), + PS384: rsa(384, 48), + PS512: rsa(512, 64), + ES256: ecdsa("P-256", 256), + ES384: ecdsa("P-384", 384), + ES512: ecdsa("P-521", 512), + EdDSA: eddsa(), + Ed25519: eddsa(), + "ML-DSA-44": mldsa(44), + "ML-DSA-65": mldsa(65), + "ML-DSA-87": mldsa(87) +}); +function jwsAlgorithm(alg) { + const entry = typeof alg == "string" ? JWS[alg] : void 0; + if (!entry) + throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`); + return entry; +} +export { + JWS, + jwsAlgorithm +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js new file mode 100644 index 000000000..76e6e02dd --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_sign.js @@ -0,0 +1,46 @@ +import { encode as b64u } from "../util/base64url.js"; +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { isDisjoint, serializeJoseHeader, validateB64, validateCrit, validateCritDuplicates, JWS_RECOGNIZED } from "./validate.js"; +import { JWSInvalid } from "../util/errors.js"; +import { concat, encode, encoder } from "./buffer_utils.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +async function createSignature(input, key, rejectUnencoded) { + let [payload, protectedHeader, unprotectedHeader, crit] = input, protectedHeaderString = ""; + if (protectedHeader !== void 0) { + const normalized = serializeJoseHeader(JWSInvalid, protectedHeader); + protectedHeader = normalized[0], protectedHeaderString = b64u(normalized[1]); + } + if (unprotectedHeader !== void 0 && (unprotectedHeader = serializeJoseHeader(JWSInvalid, unprotectedHeader)[0]), !protectedHeader && !unprotectedHeader) + throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()"); + if (!isDisjoint(protectedHeader, unprotectedHeader)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...protectedHeader, ...unprotectedHeader }; + validateCritDuplicates(JWSInvalid, protectedHeader); + const b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, crit, protectedHeader, joseHeader)); + b64 || rejectUnencoded?.(); + const { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + const entry = jwsAlgorithm(alg); + let payloadS = "", payloadB = payload, data; + if (b64) { + const encoded = input[4]; + encoded ? (payloadS = encoded[0] ??= b64u(payload), payloadB = encoded[1] ??= encode(payloadS)) : (payloadS = b64u(payload), data = encoder.encode(`${protectedHeaderString}.${payloadS}`)); + } + data ??= concat(encode(protectedHeaderString), encode("."), payloadB); + const k = await rawKey(await prepareKey(entry, key, "sign"), entry.subtle, "sign"); + entry.minRsaBits && checkModulusLength(entry.alg, k); + const jws = { + signature: b64u(new Uint8Array(await crypto.subtle.sign(entry.signing, k, data))), + payload: payloadS + }; + return protectedHeader && (jws.protected = protectedHeaderString), unprotectedHeader && (jws.header = unprotectedHeader), [jws, b64]; +} +async function createCompactSignature(payload, protectedHeader, crit, key, rejectUnencoded) { + const [jws] = await createSignature([payload, protectedHeader, void 0, crit], key, rejectUnencoded); + return `${jws.protected}.${jws.payload}.${jws.signature}`; +} +export { + createCompactSignature, + createSignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js new file mode 100644 index 000000000..7337b9c8e --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jws_verify.js @@ -0,0 +1,86 @@ +import { jwsAlgorithm } from "./jws_algorithms.js"; +import { JOSEAlgNotAllowed, JWSInvalid, JWSSignatureVerificationFailed } from "../util/errors.js"; +import { concat, decoder, encoder, encode } from "./buffer_utils.js"; +import { decodeBase64url, encodeBase64url, parseJoseHeader, isDisjoint, isObject, validateB64, validateCrit, validateAlgorithms, JWS_RECOGNIZED } from "./validate.js"; +import { prepareKey, rawKey, checkModulusLength } from "./key.js"; +function snapshotJws(jws, sharedPayload) { + const encodedProtected = jws.protected, inputHeader = jws.header, header = isObject(inputHeader) ? { ...inputHeader } : inputHeader; + let payload = sharedPayload ? sharedPayload[0] : jws.payload; + !sharedPayload && payload instanceof Uint8Array && (payload = new Uint8Array(payload)); + const signature = jws.signature, snapshot = { payload, signature }; + if (encodedProtected !== void 0 && (snapshot.protected = encodedProtected), inputHeader !== void 0 && (snapshot.header = header), encodedProtected === void 0 && header === void 0) + throw new JWSInvalid('Flattened JWS must have either of the "protected" or "header" members'); + if (encodedProtected !== void 0 && typeof encodedProtected != "string") + throw new JWSInvalid("JWS Protected Header incorrect type"); + if (payload === void 0) + throw new JWSInvalid("JWS Payload missing"); + if (typeof signature != "string") + throw new JWSInvalid("JWS Signature missing or incorrect type"); + if (header !== void 0 && !isObject(header)) + throw new JWSInvalid("JWS Unprotected Header incorrect type"); + return snapshot; +} +function prepareVerify(options) { + return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit]; +} +function parseProtectedHeader(encodedProtected) { + return encodedProtected === void 0 ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid"); +} +function encodeJsonUnencodedPayload(payload) { + const invalid = /[\p{Cs}\p{Cn}]/u.exec(payload)?.[0]; + if (invalid !== void 0) + throw new JWSInvalid(/\p{Cs}/u.test(invalid) ? "JWS Payload must be a well-formed Unicode string" : "JWS Payload must not contain unassigned Unicode code points"); + return encoder.encode(payload); +} +function encodeCompactUnencodedPayload(payload) { + try { + return encode(payload); + } catch { + throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters"); + } +} +async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) { + const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected); + if (!isDisjoint(parsedProt, header)) + throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint"); + const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader; + if (typeof alg != "string" || !alg) + throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid'); + if (shared[0] && !shared[0].has(alg)) + throw new JOSEAlgNotAllowed('"alg" (Algorithm) Header Parameter value not allowed'); + if (b64) { + if (typeof inputPayload != "string") + throw new JWSInvalid("JWS Payload must be a string"); + } else if (typeof inputPayload != "string" && !(inputPayload instanceof Uint8Array)) + throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance"); + const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload); + let resolvedKey = !1; + typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = !0); + const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== void 0 ? encode(encodedProtected) : new Uint8Array(), encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify"); + entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey); + let verified = !1; + try { + verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data); + } catch { + } + if (!verified) + throw new JWSSignatureVerificationFailed(); + const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload }; + return encodedProtected !== void 0 && (result.protectedHeader = parsedProt), header !== void 0 && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64]; +} +async function verifyCompact(jws, shared, key) { + if (jws instanceof Uint8Array && (jws = decoder.decode(jws)), typeof jws != "string") + throw new JWSInvalid("Compact JWS must be a string or Uint8Array"); + const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split("."); + if (length !== 3) + throw new JWSInvalid("Invalid Compact JWS"); + return verifySignature({ payload, protected: protectedHeader, signature }, shared, key, encodeCompactUnencodedPayload); +} +export { + encodeJsonUnencodedPayload, + parseProtectedHeader, + prepareVerify, + snapshotJws, + verifyCompact, + verifySignature +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js new file mode 100644 index 000000000..b99943f9c --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/jwt_claims_set.js @@ -0,0 +1,146 @@ +import { JWTClaimValidationFailed, JWTExpired, JWTInvalid } from "../util/errors.js"; +import { encoder, strictDecoder } from "./buffer_utils.js"; +import { isObject } from "./validate.js"; +const epoch = (date) => Math.floor(date.getTime() / 1e3), multipliers = { + s: 1, + m: 60, + h: 3600, + d: 86400, + w: 604800, + y: 31557600 +}, REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i, checkFailed = "check_failed"; +function invalidDuration() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalidDuration(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalidDuration(); + const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2; +} +function validateInput(label, input) { + if (!Number.isFinite(input)) + throw new TypeError(`Invalid ${label} input`); + return input; +} +function validateStringClaim(claim, value) { + if (typeof value != "string") + throw new TypeError(`"${claim}" claim must be a string`); +} +function validateAudienceClaim(value) { + if (typeof value != "string" && (!Array.isArray(value) || Array.from(value).some((member) => typeof member != "string"))) + throw new TypeError('"aud" claim must be a string or an array of strings'); +} +function numericDate(value, label) { + return typeof value == "number" ? validateInput(label, value) : value instanceof Date ? validateInput(label, epoch(value)) : epoch(/* @__PURE__ */ new Date()) + secs(value); +} +const normalizeTyp = (value) => { + const normalized = value.toLowerCase(); + return value.includes("/") ? normalized : `application/${normalized}`; +}, checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : !1; +function validateNumericDate(payload, claim, required = !1) { + const value = payload[claim]; + if (!(value === void 0 && !required)) { + if (typeof value != "number") + throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid"); + return value; + } +} +function unexpectedClaim(payload, claim) { + throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed); +} +function validateClaimsSet(protectedHeader, encodedPayload, options = {}) { + let payload; + try { + payload = JSON.parse(strictDecoder.decode(encodedPayload)); + } catch { + } + if (!isObject(payload)) + throw new JWTInvalid("JWT Claims Set must be a top-level JSON object"); + const { typ } = options; + if (typ !== void 0 && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) + throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed); + const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims]; + maxTokenAge !== void 0 && presenceCheck.push("iat"), audience !== void 0 && presenceCheck.push("aud"), subject !== void 0 && presenceCheck.push("sub"), issuer !== void 0 && presenceCheck.push("iss"); + for (const claim of new Set(presenceCheck.reverse())) + if (!Object.hasOwn(payload, claim)) + throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing"); + issuer !== void 0 && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== void 0 && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== void 0 && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud"); + const { clockTolerance } = options; + let tolerance = 0; + if (typeof clockTolerance == "string") + tolerance = secs(clockTolerance); + else if (clockTolerance !== void 0) { + if (typeof clockTolerance != "number") + throw new TypeError("Invalid clockTolerance option type"); + tolerance = clockTolerance; + } + validateInput("clockTolerance option", tolerance); + const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === void 0 ? /* @__PURE__ */ new Date() : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== void 0), nbf = validateNumericDate(payload, "nbf"); + if (nbf !== void 0 && nbf > now + tolerance) + throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed); + const exp = validateNumericDate(payload, "exp"); + if (exp !== void 0 && exp <= now - tolerance) + throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed); + if (maxTokenAge !== void 0) { + const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge)); + if (age - tolerance > max) + throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed); + if (age < -tolerance) + throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed); + } + return payload; +} +let producerPayloads; +function producerPayload(producer) { + return producerPayloads.get(producer); +} +function jwtData(producer) { + const payload = producerPayload(producer); + for (const claim of ["iat", "nbf", "exp"]) { + const value = payload[claim]; + if (typeof value == "number" && !Number.isFinite(value)) + throw new TypeError(`"${claim}" claim must be a finite number`); + } + return encoder.encode(JSON.stringify(payload)); +} +function jwtClaim(producer, claim) { + return producerPayload(producer)[claim]; +} +class JWTClaimsBuilder { + constructor(payload = {}) { + if (!isObject(payload)) + throw new TypeError("JWT Claims Set MUST be an object"); + (producerPayloads ||= /* @__PURE__ */ new WeakMap()).set(this, structuredClone(payload)); + } + setIssuer(value) { + return validateStringClaim("iss", value), producerPayload(this).iss = value, this; + } + setSubject(value) { + return validateStringClaim("sub", value), producerPayload(this).sub = value, this; + } + setAudience(value) { + return validateAudienceClaim(value), producerPayload(this).aud = value, this; + } + setJti(value) { + return validateStringClaim("jti", value), producerPayload(this).jti = value, this; + } + setNotBefore(value) { + return producerPayload(this).nbf = numericDate(value, "setNotBefore"), this; + } + setExpirationTime(value) { + return producerPayload(this).exp = numericDate(value, "setExpirationTime"), this; + } + setIssuedAt(value) { + const payload = producerPayload(this); + return value === void 0 ? payload.iat = epoch(/* @__PURE__ */ new Date()) : typeof value == "string" ? payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value)) : payload.iat = numericDate(value, "setIssuedAt"), this; + } +} +export { + JWTClaimsBuilder, + jwtClaim, + jwtData, + secs, + validateClaimsSet +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js new file mode 100644 index 000000000..7dc5602f1 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key.js @@ -0,0 +1,155 @@ +import { isObject } from "./validate.js"; +import { decode } from "../util/base64url.js"; +import { JOSENotSupported } from "../util/errors.js"; +const tag = (key) => key[Symbol.toStringTag], jwkMatchesOp = (entry, key, usage) => { + const { alg } = entry; + if (key.use !== void 0) { + const expected = usage === "sign" || usage === "verify" ? "sig" : "enc"; + if (key.use !== expected) + throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`); + } + if (key.alg !== void 0 && key.alg !== alg) + throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`); + if (Array.isArray(key.key_ops)) { + const expectedKeyOp = usage === "encrypt" || usage === "decrypt" ? entry.ops?.[usage === "encrypt" ? 0 : 1] : usage; + if (expectedKeyOp && !key.key_ops.includes(expectedKeyOp)) + throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`); + } +}; +async function prepareKey(entry, key, usage) { + const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign"; + if (secret && key instanceof Uint8Array) + return key; + let normalized, keyObject; + if (isObject(key)) { + if (normalized = normalizeJwk(key), typeof normalized.kty != "string") + throw invalidKeyType(alg, key, secret); + if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === void 0 && normalized.priv === void 0))) + throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`); + if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct") + return decode(normalized.k); + if (!Object.isFrozen(key)) { + const { key_ops } = key; + Array.isArray(key_ops) && Object.freeze(key_ops), Object.freeze(key); + } + } else { + if (!isKeyLike(key)) + throw invalidKeyType(alg, key, secret); + const expectedType = secret ? "secret" : privateKey ? "private" : "public"; + if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type))) + throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`); + if (isCryptoKey(key)) + return key; + if (keyObject = key, keyObject.type === "secret") + return keyObject.export(); + } + cache ||= /* @__PURE__ */ new WeakMap(); + const cacheKey = key; + let cached = cache.get(cacheKey); + if (cached?.[alg]) + return cached[alg]; + if (cached || cache.set(cacheKey, cached = {}), keyObject && typeof keyObject.toCryptoKey == "function") { + const isPublic = keyObject.type === "public", crv = nist[keyObject.asymmetricKeyDetails?.namedCurve], params = entry.resolve?.({ crv, asymmetricKeyType: keyObject.asymmetricKeyType }) ?? entry.subtle; + return cached[alg] = keyObject.toCryptoKey(params, isPublic, entry.usages[isPublic ? 0 : 1]); + } + return normalized ??= keyObject.export({ format: "jwk" }), normalized.alg = alg, cached[alg] = await jwkToKey(entry, normalized); +} +let cache; +const nist = { + __proto__: null, + prime256v1: "P-256", + secp384r1: "P-384", + secp521r1: "P-521" +}; +function assertCryptoKey(key) { + if (!isCryptoKey(key)) + throw new Error("CryptoKey instance expected"); +} +const isCryptoKey = (key) => { + if (key?.[Symbol.toStringTag] === "CryptoKey") + return !0; + try { + return key instanceof CryptoKey; + } catch { + return !1; + } +}, isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject", isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key); +function message(msg, actual, ...types) { + if (types.length > 2) { + const last = types.pop(); + msg += `one of type ${types.join(", ")}, or ${last}.`; + } else types.length === 2 ? msg += `one of type ${types[0]} or ${types[1]}.` : msg += `of type ${types[0]}.`; + return actual == null ? msg += ` Received ${actual}` : typeof actual == "function" && actual.name ? msg += ` Received function ${actual.name}` : typeof actual == "object" && actual != null && actual.constructor?.name && (msg += ` Received an instance of ${actual.constructor.name}`), msg; +} +const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types); +function invalidKeyType(alg, actual, secret) { + const types = ["CryptoKey", "KeyObject", "JSON Web Key"]; + return secret && types.push("Uint8Array"), new TypeError(message(`Key for the ${alg} algorithm must be `, actual, ...types)); +} +const unusable = (name, prop = "algorithm.name") => new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`); +function checkUsage(key, usage) { + if (usage && !key.usages.includes(usage)) + throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`); +} +function checkModulusLength(alg, key) { + const { modulusLength } = key.algorithm; + if (typeof modulusLength != "number" || modulusLength < 2048) + throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`); +} +function checkCryptoKey(key, expected, usage) { + const algorithm = key.algorithm; + if (algorithm.name !== expected.name) + throw unusable(expected.name); + if (expected.hash && algorithm.hash?.name !== expected.hash) + throw unusable(expected.hash, "algorithm.hash"); + if (expected.namedCurve && algorithm.namedCurve !== expected.namedCurve) + throw unusable(expected.namedCurve, "algorithm.namedCurve"); + if (expected.length !== void 0 && algorithm.length !== expected.length) + throw unusable(expected.length, "algorithm.length"); + checkUsage(key, usage); +} +function snapshotJwk(jwk) { + return { __proto__: null, ...jwk }; +} +function normalizeJwk(jwk) { + const normalized = snapshotJwk(jwk); + if (normalized.ext !== void 0 && typeof normalized.ext != "boolean") + throw new TypeError('"ext" (Extractable) Parameter must be a boolean'); + if (normalized.key_ops !== void 0) { + const value = normalized.key_ops, keyOps = Array.isArray(value) ? [...value] : void 0; + if (!keyOps || keyOps.some((operation) => typeof operation != "string") || new Set(keyOps).size !== keyOps.length) + throw new TypeError('"key_ops" (Key Operations) Parameter must be an array of unique strings'); + normalized.key_ops = keyOps; + } + return normalized; +} +function validateExtractableOption(extractable) { + if (extractable !== void 0 && typeof extractable != "boolean") + throw new TypeError('"extractable" option must be a boolean'); + return extractable; +} +async function jwkToKey(entry, jwk, extractable) { + if (!entry.kty.includes(jwk.kty)) + throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value'); + const algorithm = entry.resolve?.({ kty: jwk.kty, crv: jwk.crv }) ?? entry.subtle, isPrivate = !!(jwk.d || jwk.priv), keyData = { ...jwk, ext: extractable ?? jwk.ext }; + return keyData.kty !== "AKP" && delete keyData.alg, delete keyData.use, crypto.subtle.importKey("jwk", keyData, algorithm, keyData.ext ?? !isPrivate, jwk.key_ops ?? entry.usages[isPrivate ? 1 : 0]); +} +async function rawKey(key, expected, usage, extractable = !1) { + return key instanceof Uint8Array && (key = await crypto.subtle.importKey("raw", key, expected, extractable, [usage])), checkCryptoKey(key, expected, usage), key; +} +export { + assertCryptoKey, + checkCryptoKey, + checkModulusLength, + checkUsage, + invalidKeyInput, + isCryptoKey, + isKeyLike, + isKeyObject, + jwkToKey, + normalizeJwk, + prepareKey, + rawKey, + snapshotJwk, + validateExtractableOption +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js new file mode 100644 index 000000000..8a4d3fc93 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_algorithm.js @@ -0,0 +1,15 @@ +import { JOSENotSupported } from "../util/errors.js"; +import { JWS } from "./jws_algorithms.js"; +import { JWE } from "./jwe_algorithms.js"; +const algArgument = '"alg" (Algorithm)'; +function unsupportedAlg(source = 'JWK "alg" (Algorithm) Parameter') { + throw new JOSENotSupported(`Invalid or unsupported ${source} value`); +} +function keyAlgorithm(alg, source) { + return (typeof alg == "string" ? JWS[alg] ?? JWE[alg] : void 0) ?? unsupportedAlg(source); +} +export { + algArgument, + keyAlgorithm, + unsupportedAlg +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js new file mode 100644 index 000000000..cf0afbd90 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_descriptor.js @@ -0,0 +1,9 @@ +function table(entries) { + const out = { __proto__: null }; + for (const alg in entries) + out[alg] = { ...entries[alg], alg }; + return out; +} +export { + table +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js new file mode 100644 index 000000000..7b7533b70 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/key_management.js @@ -0,0 +1,196 @@ +import { encode as b64u } from "../util/base64url.js"; +import { prepareKey, rawKey, jwkToKey, checkCryptoKey, checkModulusLength, checkUsage, assertCryptoKey } from "./key.js"; +import { jweAlgorithm, jweEncryption, isJWECEKTransport } from "./jwe_algorithms.js"; +import { JOSENotSupported, JWEInvalid } from "../util/errors.js"; +import { decodeBase64url, assertUint8Array, isObject } from "./validate.js"; +import { digest, concat, encode, uint32be } from "./buffer_utils.js"; +import { checkCekLength, generateCek, encrypt, decrypt } from "./content_encryption.js"; +function checkEcdhCryptoKey(key, usage) { + if (key.algorithm.name !== "ECDH" && key.algorithm.name !== "X25519") + throw new TypeError("CryptoKey does not support this operation, its algorithm.name must be ECDH or X25519"); + checkUsage(key, usage); +} +async function aeskwWrap(alg, key, cek) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "wrapKey", !0), cryptoKeyCek = await crypto.subtle.importKey("raw", cek, { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.wrapKey("raw", cryptoKeyCek, cryptoKey, "AES-KW")); +} +async function aeskwUnwrap(alg, key, encryptedKey) { + const cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "unwrapKey", !0), cryptoKeyCek = await crypto.subtle.unwrapKey("raw", encryptedKey, cryptoKey, "AES-KW", { hash: "SHA-256", name: "HMAC" }, !0, ["sign"]); + return new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKeyCek)); +} +function checkRsaKey(alg, key, usage) { + checkCryptoKey(key, jweAlgorithm(alg).subtle, usage), checkModulusLength(alg, key); +} +async function deriveKey(p2s, alg, p2c, key) { + if (!(p2s instanceof Uint8Array) || p2s.length < 8) + throw new JWEInvalid("PBES2 Salt Input must be 8 or more octets"); + if (!Number.isSafeInteger(p2c) || Math.sign(p2c) !== 1) + throw new JWEInvalid("PBES2 Count Input must be a positive integer"); + const salt = concat(encode(alg), Uint8Array.of(0), p2s), keylen = parseInt(alg.slice(13, 16), 10), subtleAlg = { + hash: `SHA-${alg.slice(8, 11)}`, + iterations: p2c, + name: "PBKDF2", + salt + }, cryptoKey = await rawKey(key, jweAlgorithm(alg).subtle, "deriveBits"); + return new Uint8Array(await crypto.subtle.deriveBits(subtleAlg, cryptoKey, keylen)); +} +function lengthAndInput(input) { + return concat(uint32be(input.length), input); +} +async function concatKdf(Z, L, OtherInfo) { + const dkLen = L >> 3, hashLen = 32, reps = Math.ceil(dkLen / hashLen), dk = new Uint8Array(reps * hashLen); + for (let i = 1; i <= reps; i++) { + const hashResult = await digest("sha256", concat(uint32be(i), Z, OtherInfo)); + dk.set(hashResult, (i - 1) * hashLen); + } + return dk.slice(0, dkLen); +} +async function ecdhesDeriveKey(publicKey, privateKey, algorithm, keyLength, apu = new Uint8Array(), apv = new Uint8Array()) { + checkEcdhCryptoKey(publicKey), checkEcdhCryptoKey(privateKey, "deriveBits"); + const otherInfo = concat(lengthAndInput(encode(algorithm)), lengthAndInput(apu), lengthAndInput(apv), uint32be(keyLength)), Z = new Uint8Array(await crypto.subtle.deriveBits({ + name: publicKey.algorithm.name, + public: publicKey + }, privateKey, publicKey.algorithm.name === "X25519" ? 256 : Math.ceil(parseInt(publicKey.algorithm.namedCurve.slice(-3), 10) / 8) << 3)); + return concatKdf(Z, keyLength, otherInfo); +} +function assertEcdhKey(key) { + assertCryptoKey(key); + const curve = key.algorithm.namedCurve; + if (curve !== "P-256" && curve !== "P-384" && curve !== "P-521" && key.algorithm.name !== "X25519") + throw new JOSENotSupported("ECDH with the provided key is not allowed or not supported by your javascript runtime"); +} +function partyInfo(joseHeader, name) { + const value = joseHeader[name]; + if (value !== void 0) { + if (typeof value != "string") + throw new JWEInvalid(`JOSE Header "${name}" (Agreement Party${name === "apu" ? "U" : "V"}Info) invalid`); + return decodeBase64url(value, name, JWEInvalid); + } +} +function checkPartyInfo(apu, apv) { + if (!(apu === void 0 || apv === void 0 || apu.byteLength !== apv.byteLength)) { + for (let i = 0; i < apu.byteLength; i++) + if (apu[i] !== apv[i]) + return; + throw new JWEInvalid('JOSE Header "apu" and "apv" values must be distinct'); + } +} +function assertEncryptedKey(encryptedKey) { + if (encryptedKey === void 0) + throw new JWEInvalid("JWE Encrypted Key missing"); +} +function assertNoEncryptedKey(encryptedKey) { + if (encryptedKey !== void 0) + throw new JWEInvalid("Encountered unexpected JWE Encrypted Key"); +} +function validateMaxPBES2Count(value) { + if (value !== void 0 && value !== 1 / 0 && (!Number.isSafeInteger(value) || value < 1)) + throw new TypeError("maxPBES2Count must be a positive safe integer or Infinity"); +} +async function decryptKeyManagement(entry, enc, key, encryptedKey, joseHeader, maxPBES2Count) { + const { alg } = entry, mode = entry.mode; + if (mode === "direct-encryption") + return assertNoEncryptedKey(encryptedKey), key; + const direct = mode === "direct-key-agreement"; + switch (direct ? assertNoEncryptedKey(encryptedKey) : assertEncryptedKey(encryptedKey), entry.subtle.name) { + case "ECDH": { + const { epk } = joseHeader; + if (!isObject(epk) || ["d", "k", "p", "q", "dp", "dq", "qi", "oth", "priv"].some((parameter) => Object.hasOwn(epk, parameter))) + throw new JWEInvalid('JOSE Header "epk" (Ephemeral Public Key) missing or invalid'); + assertEcdhKey(key); + const ephemeralPublicKey = await jwkToKey(entry, epk), partyUInfo = partyInfo(joseHeader, "apu"), partyVInfo = partyInfo(joseHeader, "apv"); + checkPartyInfo(partyUInfo, partyVInfo); + const sharedSecret = await ecdhesDeriveKey(ephemeralPublicKey, key, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), partyUInfo, partyVInfo); + if (direct) + return sharedSecret; + key = sharedSecret; + break; + } + case "RSA-OAEP": + return assertCryptoKey(key), checkRsaKey(alg, key, "decrypt"), new Uint8Array(await crypto.subtle.decrypt("RSA-OAEP", key, encryptedKey)); + case "PBKDF2": { + if (typeof joseHeader.p2c != "number") + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) missing or invalid'); + validateMaxPBES2Count(maxPBES2Count); + const p2cLimit = maxPBES2Count ?? 1e4; + if (joseHeader.p2c > p2cLimit) + throw new JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds'); + if (typeof joseHeader.p2s != "string") + throw new JWEInvalid('JOSE Header "p2s" (PBES2 Salt) missing or invalid'); + const p2s = decodeBase64url(joseHeader.p2s, "p2s", JWEInvalid); + key = await deriveKey(p2s, alg, joseHeader.p2c, key); + break; + } + case "AES-GCM": { + if (typeof joseHeader.iv != "string") + throw new JWEInvalid('JOSE Header "iv" (Initialization Vector) missing or invalid'); + if (typeof joseHeader.tag != "string") + throw new JWEInvalid('JOSE Header "tag" (Authentication Tag) missing or invalid'); + const iv = decodeBase64url(joseHeader.iv, "iv", JWEInvalid), tag = decodeBase64url(joseHeader.tag, "tag", JWEInvalid); + if (iv.byteLength !== 12) + throw new JWEInvalid("Invalid Initialization Vector length"); + if (tag.byteLength !== 16) + throw new JWEInvalid("Invalid Authentication Tag length"); + return decrypt(jweEncryption(alg.slice(0, -2)), key, encryptedKey, iv, tag, new Uint8Array()); + } + } + return aeskwUnwrap(alg.slice(-6), key, encryptedKey); +} +async function encryptKeyManagement(entry, enc, inputKey, joseHeader, providedCek, providedParameters = {}) { + const { alg, mode } = entry, transport = isJWECEKTransport(entry); + if (providedCek !== void 0 && !transport) + throw new TypeError(`setContentEncryptionKey cannot be called with JWE "alg" (Algorithm) Header ${alg}`); + let key = await prepareKey(mode === "direct-encryption" ? enc : entry, inputKey, "encrypt"); + if (mode === "direct-encryption") + return [key, void 0, void 0]; + const cek = transport ? providedCek ?? generateCek(enc) : void 0; + cek && checkCekLength(cek, enc.cekBits); + let encryptedKey, parameters; + switch (entry.subtle.name) { + case "ECDH": { + assertEcdhKey(key); + const { apu: providedApu, apv: providedApv } = providedParameters; + providedApu !== void 0 && assertUint8Array(providedApu, '"apu"'), providedApv !== void 0 && assertUint8Array(providedApv, '"apv"'); + const apu = providedApu ?? partyInfo(joseHeader, "apu"), apv = providedApv ?? partyInfo(joseHeader, "apv"); + checkPartyInfo(apu, apv); + let ephemeralKey; + providedParameters.epk !== void 0 ? ephemeralKey = await prepareKey(entry, providedParameters.epk, "decrypt") : ephemeralKey = (await crypto.subtle.generateKey(key.algorithm, !0, ["deriveBits"])).privateKey; + const subtle = crypto.subtle; + let exportableEpk = ephemeralKey; + if (!exportableEpk.extractable) { + if (typeof subtle.getPublicKey != "function") + throw new TypeError('CryptoKey for "epk" must be extractable'); + exportableEpk = await subtle.getPublicKey(ephemeralKey, []); + } + const { x, y, crv, kty } = await subtle.exportKey("jwk", exportableEpk), direct = mode === "direct-key-agreement", sharedSecret = await ecdhesDeriveKey(key, ephemeralKey, direct ? enc.alg : alg, direct ? enc.cekBits : parseInt(alg.slice(-5, -2), 10), apu, apv), epk = { x, crv, kty }; + if (kty === "EC" && (epk.y = y), parameters = { epk }, providedApu !== void 0 && (parameters.apu = b64u(providedApu)), providedApv !== void 0 && (parameters.apv = b64u(providedApv)), direct) + return [sharedSecret, void 0, parameters]; + key = sharedSecret; + break; + } + case "RSA-OAEP": { + assertCryptoKey(key), checkRsaKey(alg, key, "encrypt"), encryptedKey = new Uint8Array(await crypto.subtle.encrypt("RSA-OAEP", key, cek)); + break; + } + case "PBKDF2": { + const { p2c = 2048, p2s = crypto.getRandomValues(new Uint8Array(16)) } = providedParameters; + key = await deriveKey(p2s, alg, p2c, key), parameters = { p2c, p2s: b64u(p2s) }; + break; + } + case "AES-GCM": { + const iv = providedParameters.iv === void 0 ? crypto.getRandomValues(new Uint8Array(12)) : providedParameters.iv; + if (!(iv instanceof Uint8Array)) + throw new TypeError('"iv" must be an instance of Uint8Array'); + const wrapped = await encrypt(jweEncryption(alg.slice(0, -2)), cek, key, iv, new Uint8Array()); + encryptedKey = wrapped.ciphertext, parameters = { iv: b64u(wrapped.iv), tag: b64u(wrapped.tag) }; + } + } + if (encryptedKey ??= await aeskwWrap(alg.slice(-6), key, cek), !(encryptedKey instanceof Uint8Array) || !encryptedKey.byteLength) + throw new TypeError("JWE key management algorithm did not produce an Encrypted Key"); + return [cek, encryptedKey, parameters]; +} +export { + decryptKeyManagement, + encryptKeyManagement, + validateMaxPBES2Count +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js new file mode 100644 index 000000000..51b7dfe89 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/lib/validate.js @@ -0,0 +1,122 @@ +import { JOSENotSupported, JWSInvalid } from "../util/errors.js"; +import { decode } from "../util/base64url.js"; +import { encode, strictDecoder } from "./buffer_utils.js"; +function assertUint8Array(input, label) { + if (!(input instanceof Uint8Array)) + throw new TypeError(`${label} must be an instance of Uint8Array`); +} +function isObject(input) { + if (typeof input != "object" || input === null || Object.prototype.toString.call(input) !== "[object Object]") + return !1; + const prototype = Object.getPrototypeOf(input); + return prototype === null || Object.getPrototypeOf(prototype) === null; +} +function isJwkSet(input) { + return isObject(input) && Array.isArray(input.keys) && Array.from(input.keys).every(isObject); +} +function isDisjoint(...headers) { + const parameters = /* @__PURE__ */ new Set(); + for (const header of headers) + if (header) + for (const parameter of Object.keys(header)) { + if (parameters.has(parameter)) + return !1; + parameters.add(parameter); + } + return !0; +} +function assertNotSet(value, name) { + if (value !== void 0) + throw new TypeError(`${name} can only be called once`); +} +function decodeBase64url(value, label, ErrorClass) { + try { + return decode(value); + } catch { + throw new ErrorClass(`Failed to base64url decode the ${label}`); + } +} +function encodeBase64url(value, label, ErrorClass) { + try { + return encode(value); + } catch { + throw new ErrorClass(`The ${label} is not a valid base64url string`); + } +} +function parseJoseHeader(b64, ErrorClass, message) { + let parsed; + try { + parsed = JSON.parse(strictDecoder.decode(decode(b64))); + } catch { + throw new ErrorClass(message); + } + if (!isObject(parsed)) + throw new ErrorClass(message); + return parsed; +} +const JWS_RECOGNIZED = { __proto__: null, b64: !0 }, JWE_RECOGNIZED = { __proto__: null }; +function validateAlgorithms(option, algorithms) { + if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s != "string"))) + throw new TypeError(`"${option}" option must be an array of strings`); + return algorithms === void 0 ? void 0 : new Set(algorithms); +} +function validateCritDuplicates(Err, protectedHeader) { + const { crit } = protectedHeader ?? {}; + if (Array.isArray(crit) && new Set(crit).size !== crit.length) + throw new Err('"crit" (Critical) Header Parameter MUST NOT contain duplicate values'); +} +function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) { + if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) + throw new Err('"crit" (Critical) Header Parameter MUST be integrity protected'); + if (!protectedHeader || protectedHeader.crit === void 0) + return []; + if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input != "string" || input.length === 0)) + throw new Err('"crit" (Critical) Header Parameter MUST be an array of non-empty strings when present'); + const recognized = recognizedOption === void 0 ? recognizedDefault : { __proto__: null, ...recognizedOption, ...recognizedDefault }; + for (const parameter of protectedHeader.crit) { + if (!(parameter in recognized)) + throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`); + if (!Object.hasOwn(joseHeader, parameter) || joseHeader[parameter] === void 0) + throw new Err(`Extension Header Parameter "${parameter}" is missing`); + if (recognized[parameter] && (!Object.hasOwn(protectedHeader, parameter) || protectedHeader[parameter] === void 0)) + throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`); + } + return protectedHeader.crit; +} +function validateB64(protectedHeader, extensions) { + if (extensions.includes("b64")) { + const b64 = protectedHeader.b64; + if (typeof b64 != "boolean") + throw new JWSInvalid('The "b64" (base64url-encode payload) Header Parameter must be a boolean'); + return b64; + } + return !0; +} +function serializeJoseHeader(Err, header) { + let serialized, parsed; + try { + serialized = JSON.stringify(header), parsed = JSON.parse(serialized); + } catch (cause) { + throw new Err("JOSE Header is not valid JSON", { cause }); + } + if (!isObject(parsed)) + throw new Err("JOSE Header is not a JSON object"); + return [parsed, serialized]; +} +export { + JWE_RECOGNIZED, + JWS_RECOGNIZED, + assertNotSet, + assertUint8Array, + decodeBase64url, + encodeBase64url, + isDisjoint, + isJwkSet, + isObject, + parseJoseHeader, + serializeJoseHeader, + validateAlgorithms, + validateB64, + validateCrit, + validateCritDuplicates +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js new file mode 100644 index 000000000..4447baa6d --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/base64url.js @@ -0,0 +1,16 @@ +import { encoder, decoder, encodeBase64, decodeBase64 } from "../lib/buffer_utils.js"; +const invalid = "The input to be decoded is not correctly encoded."; +function decode(input) { + try { + return decodeBase64(typeof input == "string" ? input : decoder.decode(input), !0); + } catch (cause) { + throw new TypeError(invalid, { cause }); + } +} +function encode(input) { + return encodeBase64(typeof input == "string" ? encoder.encode(input) : input, !0); +} +export { + decode, + encode +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js new file mode 100644 index 000000000..a4b277f18 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_jwt.js @@ -0,0 +1,33 @@ +import { decode as b64u } from "./base64url.js"; +import { strictDecoder } from "../lib/buffer_utils.js"; +import { isObject } from "../lib/validate.js"; +import { JWTInvalid } from "./errors.js"; +function decodeJwt(jwt) { + if (typeof jwt != "string") + throw new JWTInvalid("JWTs must use Compact JWS serialization, JWT must be a string"); + const { 1: payload, length } = jwt.split("."); + if (length === 5) + throw new JWTInvalid("Only JWTs using Compact JWS serialization can be decoded"); + if (length !== 3) + throw new JWTInvalid("Invalid JWT"); + if (!payload) + throw new JWTInvalid("JWTs must contain a payload"); + let decoded; + try { + decoded = b64u(payload); + } catch { + throw new JWTInvalid("Failed to base64url decode the payload"); + } + let result; + try { + result = JSON.parse(strictDecoder.decode(decoded)); + } catch { + throw new JWTInvalid("Failed to parse the decoded payload as JSON"); + } + if (!isObject(result)) + throw new JWTInvalid("Invalid JWT Claims Set"); + return result; +} +export { + decodeJwt +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js new file mode 100644 index 000000000..274e03617 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/decode_protected_header.js @@ -0,0 +1,19 @@ +import { parseJoseHeader } from "../lib/validate.js"; +function decodeProtectedHeader(token) { + let protectedB64u; + if (typeof token == "string") { + const parts = token.split("."); + (parts.length === 3 || parts.length === 5) && ([protectedB64u] = parts); + } else if (typeof token == "object" && token) + if ("protected" in token) + protectedB64u = token.protected; + else + throw new TypeError("Token does not contain a Protected Header"); + const invalid = "Invalid Token or Protected Header formatting"; + if (typeof protectedB64u != "string" || !protectedB64u) + throw new TypeError(invalid); + return parseJoseHeader(protectedB64u, TypeError, invalid); +} +export { + decodeProtectedHeader +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js new file mode 100644 index 000000000..b7d579f51 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/dist/webapi/util/errors.js @@ -0,0 +1,109 @@ +class JOSEError extends Error { + static code = "ERR_JOSE_GENERIC"; + code = "ERR_JOSE_GENERIC"; + constructor(message, options) { + super(message, options), this.name = this.constructor.name, Error.captureStackTrace?.(this, this.constructor); + } +} +class JWTClaimValidationFailed extends JOSEError { + static code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + code = "ERR_JWT_CLAIM_VALIDATION_FAILED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JWTExpired extends JOSEError { + static code = "ERR_JWT_EXPIRED"; + code = "ERR_JWT_EXPIRED"; + claim; + reason; + payload; + constructor(message, payload, claim = "unspecified", reason = "unspecified") { + super(message, { cause: { claim, reason, payload } }), this.claim = claim, this.reason = reason, this.payload = payload; + } +} +class JOSEAlgNotAllowed extends JOSEError { + static code = "ERR_JOSE_ALG_NOT_ALLOWED"; + code = "ERR_JOSE_ALG_NOT_ALLOWED"; +} +class JOSENotSupported extends JOSEError { + static code = "ERR_JOSE_NOT_SUPPORTED"; + code = "ERR_JOSE_NOT_SUPPORTED"; +} +class JWEDecryptionFailed extends JOSEError { + static code = "ERR_JWE_DECRYPTION_FAILED"; + code = "ERR_JWE_DECRYPTION_FAILED"; + constructor(message = "decryption operation failed", options) { + super(message, options); + } +} +class JWEInvalid extends JOSEError { + static code = "ERR_JWE_INVALID"; + code = "ERR_JWE_INVALID"; +} +class JWSInvalid extends JOSEError { + static code = "ERR_JWS_INVALID"; + code = "ERR_JWS_INVALID"; +} +class JWTInvalid extends JOSEError { + static code = "ERR_JWT_INVALID"; + code = "ERR_JWT_INVALID"; +} +class JWKInvalid extends JOSEError { + static code = "ERR_JWK_INVALID"; + code = "ERR_JWK_INVALID"; +} +class JWKSInvalid extends JOSEError { + static code = "ERR_JWKS_INVALID"; + code = "ERR_JWKS_INVALID"; +} +class JWKSNoMatchingKey extends JOSEError { + static code = "ERR_JWKS_NO_MATCHING_KEY"; + code = "ERR_JWKS_NO_MATCHING_KEY"; + constructor(message = "no applicable key found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSMultipleMatchingKeys extends JOSEError { + [Symbol.asyncIterator] = async function* () { + }; + static code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + code = "ERR_JWKS_MULTIPLE_MATCHING_KEYS"; + constructor(message = "multiple matching keys found in the JSON Web Key Set", options) { + super(message, options); + } +} +class JWKSTimeout extends JOSEError { + static code = "ERR_JWKS_TIMEOUT"; + code = "ERR_JWKS_TIMEOUT"; + constructor(message = "request timed out", options) { + super(message, options); + } +} +class JWSSignatureVerificationFailed extends JOSEError { + static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED"; + constructor(message = "signature verification failed", options) { + super(message, options); + } +} +export { + JOSEAlgNotAllowed, + JOSEError, + JOSENotSupported, + JWEDecryptionFailed, + JWEInvalid, + JWKInvalid, + JWKSInvalid, + JWKSMultipleMatchingKeys, + JWKSNoMatchingKey, + JWKSTimeout, + JWSInvalid, + JWSSignatureVerificationFailed, + JWTClaimValidationFailed, + JWTExpired, + JWTInvalid +}; diff --git a/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json new file mode 100644 index 000000000..2d7d92f72 --- /dev/null +++ b/tests/corpus/6331-jose-root-entry/node_modules/jose/package.json @@ -0,0 +1,134 @@ +{ + "name": "jose", + "version": "6.2.12", + "description": "JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes", + "keywords": [ + "akp", + "browser", + "bun", + "cloudflare", + "compact", + "decode", + "decrypt", + "deno", + "detached", + "ec", + "ecdsa", + "ed25519", + "eddsa", + "edge", + "electron", + "embedded", + "encrypt", + "flattened", + "general", + "jose", + "json web token", + "jsonwebtoken", + "jwa", + "jwe", + "jwk", + "jwks", + "jws", + "jwt-decode", + "jwt", + "ml-dsa", + "netlify", + "next", + "nextjs", + "oct", + "okp", + "payload", + "pem", + "pkcs8", + "rsa", + "sign", + "signature", + "spki", + "validate", + "vercel", + "verify", + "webcrypto", + "workerd", + "workers", + "x509" + ], + "homepage": "https://github.com/panva/jose", + "repository": "panva/jose", + "funding": { + "url": "https://github.com/sponsors/panva" + }, + "license": "MIT", + "author": "Filip Skokan ", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/webapi/index.js" + }, + "./jwk/*": { + "types": "./dist/types/jwk/*.d.ts", + "default": "./dist/webapi/jwk/*.js" + }, + "./key/import": { + "types": "./dist/types/key/import.d.ts", + "default": "./dist/webapi/key/import.js" + }, + "./key/export": { + "types": "./dist/types/key/export.d.ts", + "default": "./dist/webapi/key/export.js" + }, + "./key/generate/keypair": { + "types": "./dist/types/key/generate_key_pair.d.ts", + "default": "./dist/webapi/key/generate_key_pair.js" + }, + "./key/generate/secret": { + "types": "./dist/types/key/generate_secret.d.ts", + "default": "./dist/webapi/key/generate_secret.js" + }, + "./jwks/*": { + "types": "./dist/types/jwks/*.d.ts", + "default": "./dist/webapi/jwks/*.js" + }, + "./jwt/decode": { + "types": "./dist/types/util/decode_jwt.d.ts", + "default": "./dist/webapi/util/decode_jwt.js" + }, + "./jwt/*": { + "types": "./dist/types/jwt/*.d.ts", + "default": "./dist/webapi/jwt/*.js" + }, + "./decode/protected_header": { + "types": "./dist/types/util/decode_protected_header.d.ts", + "default": "./dist/webapi/util/decode_protected_header.js" + }, + "./jws/*": { + "types": "./dist/types/jws/*.d.ts", + "default": "./dist/webapi/jws/*.js" + }, + "./jwe/*": { + "types": "./dist/types/jwe/*.d.ts", + "default": "./dist/webapi/jwe/*.js" + }, + "./errors": { + "types": "./dist/types/util/errors.d.ts", + "default": "./dist/webapi/util/errors.js" + }, + "./base64url": { + "types": "./dist/types/util/base64url.d.ts", + "default": "./dist/webapi/util/base64url.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/webapi/index.js", + "types": "./dist/types/index.d.ts", + "files": [ + "dist/webapi/**/*.js", + "dist/types/**/*.d.ts", + "!dist/**/*.bundle.js", + "!dist/**/*.umd.js", + "!dist/**/*.min.js", + "!dist/types/lib/**" + ] +} diff --git a/tests/corpus/6332-fluent-this-declaration/main.ts b/tests/corpus/6332-fluent-this-declaration/main.ts new file mode 100644 index 000000000..10804083f --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/main.ts @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s: Signer): string { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6332-fluent-this-declaration/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/main.mjs b/tests/corpus/6333-fluent-this-declaration-js/main.mjs new file mode 100644 index 000000000..44ac698cc --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/main.mjs @@ -0,0 +1,17 @@ +// A declaration file types a JavaScript class's inherited setters as +// returning `this` (jose's SignJWT over JWTClaimsBuilder): chained calls +// on the subclass keep the subclass, so its own methods stay callable. +import { Counter, Signer } from "fluent"; + +console.log(new Signer({ role: "admin" }).setHeader("HS256").setSubject("alice").setIssuer("me").sign()); +console.log(new Signer().setSubject("bob").setHeader("HS384").sign()); +const signer = new Signer({ n: 1 }).setIssuer("you").setSubject("carol"); +console.log(signer.setHeader("HS512").sign()); +const counter = new Counter({ a: 1 }).setSubject("dave").setIssuer("eve"); +console.log(counter.count(), counter.setHeader("x").sign(), counter instanceof Counter); +function described(s) { + return s.setSubject("frank").sign(); +} +console.log(described(new Counter().setHeader("y"))); +const reset = new Counter({ b: 2 }).reset(); +console.log(reset.count(), reset.setSubject("gina").count()); diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts new file mode 100644 index 000000000..486f72652 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.d.ts @@ -0,0 +1,14 @@ +export interface Claims { + setSubject(subject: string): this; + setIssuer(issuer: string): this; + reset(): this; +} +declare const Signer_base: new (payload?: Record) => Claims; +export declare class Signer extends Signer_base { + #private; + setHeader(header: string): this; + sign(): string; +} +export declare class Counter extends Signer { + count(): number; +} diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js new file mode 100644 index 000000000..9334722a9 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/index.js @@ -0,0 +1,31 @@ +const claims = new WeakMap(); +class ClaimsBuilder { + constructor(payload = {}) { + claims.set(this, structuredClone(payload)); + } + setSubject(subject) { + return claims.get(this).sub = subject, this; + } + setIssuer(issuer) { + return claims.get(this).iss = issuer, this; + } + reset() { + return claims.set(this, {}), this; + } +} +const Signer_base = ClaimsBuilder; +class Signer extends Signer_base { + #header = "none"; + setHeader(header) { + return this.#header = header, this; + } + sign() { + return `${this.#header}.${JSON.stringify(claims.get(this))}`; + } +} +class Counter extends Signer { + count() { + return Object.keys(claims.get(this)).length; + } +} +export { Signer, Counter }; diff --git a/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json new file mode 100644 index 000000000..428e91d97 --- /dev/null +++ b/tests/corpus/6333-fluent-this-declaration-js/node_modules/fluent/package.json @@ -0,0 +1,11 @@ +{ + "name": "fluent", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + } +} diff --git a/tests/corpus/6334-discarded-logical-statements.mjs b/tests/corpus/6334-discarded-logical-statements.mjs new file mode 100644 index 000000000..2e1df91a8 --- /dev/null +++ b/tests/corpus/6334-discarded-logical-statements.mjs @@ -0,0 +1,42 @@ +// Minified `a && b;` / `a || b;` statements whose operands share no value +// representation lower as the control flow they are (jose's secs()), and +// element reads on a RegExpExecArray | null the code ruled non-null +// without narrowing answer undefined for groups that did not participate. +const multipliers = { s: 1, m: 60, h: 3600, d: 86400 }; +const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|s|minutes?|m|hours?|h|days?|d)(?: (ago|from now))?$/i; +function invalid() { + throw new TypeError("Invalid time period format"); +} +function secs(str) { + typeof str != "string" && invalid(); + const matched = REGEX.exec(str); + (!matched || matched[4] && matched[1]) && invalid(); + const value = parseFloat(matched[2]); + const n = Math.round(value * multipliers[matched[3][0].toLowerCase()]); + return Number.isFinite(n) || invalid(), matched[1] === "-" || matched[4] === "ago" ? -n : n; +} +for (const input of ["10 minutes", "2h", "-1d", "+3 s", "1.5 hours ago", "4 days from now", "-2h ago", "bogus", "", 7]) { + try { + console.log(JSON.stringify(input), secs(input)); + } catch (e) { + console.log(JSON.stringify(input), e.name, e.message); + } +} + +const log = []; +function note(v) { + log.push(v); + return v; +} +const values = [0, 1, "", "x", undefined, null, true, false]; +for (const v of values) { + (note(v) || typeof v === "number") && note("and"); + (v && typeof v === "string") || note("or"); + !v || (v === true && note("bool")) || note("rest"); +} +console.log(JSON.stringify(log)); + +const m = /(a)?(b)(c)?/.exec("b"); +console.log(m[1], m[2], m[3], m[7], m[1] === undefined, m.length); +const none = /(z)/.exec("b"); +console.log(none?.[1], none === null); diff --git a/tests/corpus/6335-discarded-logical-statements.ts b/tests/corpus/6335-discarded-logical-statements.ts new file mode 100644 index 000000000..84b89f7b1 --- /dev/null +++ b/tests/corpus/6335-discarded-logical-statements.ts @@ -0,0 +1,27 @@ +// `a && b;` / `a || b;` statements whose operands share no value +// representation lower as control flow: only the deciding test runs. +function invalid(): never { + throw new TypeError("invalid"); +} +function check(input: string): string { + const matched = /^(\+|-)?(\d+)(s|m)( ago)?$/.exec(input); + (!matched || (matched[4] && matched[1])) && invalid(); + return matched ? `${matched[2]}${matched[3]}` : "none"; +} +for (const input of ["5s", "-5m", "5m ago", "-5m ago", "x"]) { + try { + console.log(input, check(input)); + } catch (e) { + console.log(input, (e as Error).message); + } +} +const seen: string[] = []; +function flag(s: string, v: boolean | string | undefined): boolean | string | undefined { + seen.push(s); + return v; +} +for (const v of [true, false, "", "y", undefined]) { + (flag("a", v) || flag("b", v === undefined ? "u" : false)) && seen.push("then"); + (flag("c", v) && flag("d", typeof v === "string")) || seen.push("else"); +} +console.log(seen.join(",")); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 72e53b3f5..78fbf2af7 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -104,4 +104,8 @@ async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoK await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); } void encryptWithWebCrypto; +/* Node's navigator global: typeof navigator and navigator.userAgent + * compile; the other members fence by name. */ +const cores = navigator.hardwareConcurrency; +void cores; // End of the declared-but-not-lowered surface. diff --git a/tests/fixtures/node-types/navigator.ts b/tests/fixtures/node-types/navigator.ts new file mode 100644 index 000000000..2f1ee2d9f --- /dev/null +++ b/tests/fixtures/node-types/navigator.ts @@ -0,0 +1,6 @@ +/* Node 24's navigator global under @types/node: the runtime probe + * libraries use (jose's user-agent check) folds to Node's values. */ +const browser = !navigator.userAgent.startsWith("Mozilla/5.0 "); +console.log(typeof navigator, navigator.userAgent, browser); +console.log(typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"); +console.log(navigator.userAgent.split("/")[0], navigator.userAgent.length); diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 913b78198..81db57e65 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -221,4 +221,13 @@ fenced.ts:104:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard | ^~~~~~~~~~~~~~~~~~~~~ 105 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + +fenced.ts:109:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet + + 108 | * compile; the other members fence by name. */ + 109 | const cores = navigator.hardwareConcurrency; + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 110 | void cores; + + hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json new file mode 100644 index 000000000..c6ea1665b --- /dev/null +++ b/tests/harness/corpus-costs/6331-jose-root-entry/main.ts.json @@ -0,0 +1 @@ +{ "plain": 20300, "san": 40800 } diff --git a/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6332-fluent-this-declaration/main.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6333-fluent-this-declaration-js/main.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6334-discarded-logical-statements.mjs.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json new file mode 100644 index 000000000..8c0e66c3c --- /dev/null +++ b/tests/harness/corpus-costs/6335-discarded-logical-statements.ts.json @@ -0,0 +1 @@ +{ "plain": 3000, "san": 3600 } diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 769ad702d..802fbd3e1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -857,6 +857,24 @@ test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { + const outDir = outDirFor("node-navigator"); + const entry = join(nodeTypesDir, "navigator.ts"); + const result = await compile(entry, { + outPath: join(outDir, "navigator"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: declared-but-not-lowered surface fences, naming @types/node", async () => { const outDir = outDirFor("node-fenced"); const result = await compile(join(nodeTypesDir, "fenced.ts"), { From 1a61c4daa0bc1e844f87d4debab34a427130ebf9 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 10:51:12 -0700 Subject: [PATCH 4/4] Keep the verdict-cache test's blocker refused now that array position patterns compile The test used `const { length } = [1, 2] as number[]` as its coverage blocker. That compiles now, so no diagnostic was left to replay. The blocker destructures a `number[] | string` union instead, which keeps the SC1031 object-pattern fence. The comment-only replay and code-edit relower assertions are unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/coverage/verdict-cache.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/coverage/verdict-cache.test.ts b/packages/compiler/src/coverage/verdict-cache.test.ts index 1c3e9bc3d..0380e8b8d 100644 --- a/packages/compiler/src/coverage/verdict-cache.test.ts +++ b/packages/compiler/src/coverage/verdict-cache.test.ts @@ -11,7 +11,8 @@ afterAll(() => rmSync(dir, { recursive: true, force: true })); test("unchanged and comment-only reruns replay the verdict; code edits relower", () => { const entry = join(dir, "main.ts"); - const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = [1, 2] as number[];\nconsole.log(length);\n`; + // The blocker: an object pattern over a union source (arrays alone compile). + const program = `const pairs: [number, number][] = [[1, 2]];\nconst [[a, b]] = pairs;\nconsole.log(a + b);\nconst { length } = (a > 9 ? [1, 2] : "ab") as number[] | string;\nconsole.log(length);\n`; writeFileSync(entry, program); const entries = new Map(); const store: VerdictStore = {