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.json b/docs/src/generated/node-v24-compatibility.json index 402e161fb..be1a931cc 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": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -854,9 +854,9 @@ "apiEntries": 156, "static": { "supported": 2, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 144, + "not-implemented": 133, "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" } }, { @@ -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" } }, { @@ -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 75e789c61..a02446908 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": 3454, - "tierItemCount": 3454, + "taskCount": 3453, + "tierItemCount": 3453, "tiers": { "static": { - "audit-partial": 1165, - "verify-gap": 2161, + "audit-partial": 1179, + "verify-gap": 2146, "replace-refusal": 41, "classify": 2, "implement": 85 @@ -17,7 +17,7 @@ "priorities": { "high": 2287, "normal": 694, - "low": 473 + "low": 472 } }, "tasks": [ @@ -33201,12 +33201,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" + ] } } }, @@ -33932,12 +33938,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" + ] } } }, @@ -34853,12 +34865,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" + ] } } }, @@ -34987,31 +35003,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", @@ -81024,12 +81015,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" + ] } } }, @@ -81049,12 +81046,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" + ] } } }, @@ -81074,12 +81075,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" + ] } } }, @@ -81099,12 +81104,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" + ] } } }, @@ -81124,12 +81133,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" + ] } } }, @@ -81149,12 +81162,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" + ] } } }, @@ -81249,12 +81266,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" + ] } } }, @@ -81399,12 +81422,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" + ] } } }, @@ -81574,12 +81604,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" + ] } } }, @@ -81599,12 +81636,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" + ] } } }, @@ -81649,12 +81693,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 c93030602..3690ce077 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": 12, + "supported": 1, + "partial": 15, "refused": 1, - "not-implemented": 79, + "not-implemented": 75, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -911,9 +911,9 @@ "apiEntries": 156, "static": { "supported": 2, - "partial": 10, + "partial": 21, "refused": 0, - "not-implemented": 144, + "not-implemented": 133, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -52242,8 +52242,16 @@ "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/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" @@ -52664,8 +52672,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" }, @@ -53338,8 +53353,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" }, @@ -54244,8 +54266,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" }, @@ -54382,8 +54409,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" }, @@ -106872,10 +106903,16 @@ "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.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" ] }, @@ -110411,8 +110448,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" }, @@ -110434,8 +110478,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" }, @@ -110457,8 +110506,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" }, @@ -110480,8 +110534,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" }, @@ -110503,8 +110562,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" }, @@ -110526,8 +110590,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" }, @@ -110618,8 +110687,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" }, @@ -110756,8 +110832,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" }, @@ -110917,8 +111001,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" }, @@ -110940,8 +111032,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" }, @@ -110986,8 +111086,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 71605612e..855d065f7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -179,11 +179,19 @@ "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": ["`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"]}, {"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 3b522dd3e..a8f1f8a8b 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 a565965f1..2651876fc 100644 --- a/packages/compiler/src/backend/executable-features.ts +++ b/packages/compiler/src/backend/executable-features.ts @@ -40,5 +40,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 daa8e08f3..3a0e007db 100644 --- a/packages/compiler/src/backend/llvm/lib-dispatch.ts +++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts @@ -832,6 +832,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 73b6d72e7..7bbb97814 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -588,6 +588,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", @@ -1471,6 +1476,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 3beb84a20..756ec7ccf 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -51,6 +51,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 875a723bc..e7ba3285c 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -199,6 +199,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 6c8673fe4..a99ee47cd 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -578,6 +578,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/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 = { 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 ee9d84fcf..7f64f6614 100644 --- a/packages/compiler/src/executable/executable-cache.ts +++ b/packages/compiler/src/executable/executable-cache.ts @@ -80,6 +80,7 @@ export interface EarlyExecutableNativeFeatures { tls: boolean; cipher: boolean; tlsCa: boolean; + webcrypto: boolean; } interface EarlyExecutableCacheStamp { @@ -193,6 +194,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/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/builtins/strings.ts b/packages/compiler/src/frontend/lowering/builtins/strings.ts index 013cd3fda..54ab22c05 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); @@ -108,6 +122,99 @@ 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.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) + return null; + const listNode = call.arguments.nodes[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.nodes[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..2a9ff9eef --- /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.nodes.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.nodes[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.nodes.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 437f01cfb..707b60481 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -686,12 +686,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-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 95a3ae5ab..9bae6f682 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; @@ -6970,19 +7003,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 b088b89bc..bbf1d6087 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -6,13 +6,14 @@ 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"; 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"; @@ -221,6 +222,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 { @@ -362,6 +364,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; @@ -650,6 +672,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)) { @@ -1697,6 +1721,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. @@ -1960,6 +1992,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). @@ -2158,6 +2194,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; @@ -7086,6 +7126,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 @@ -8047,6 +8091,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; @@ -12914,6 +12974,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; } @@ -15396,6 +15458,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-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 89afd2d82..71ea52f75 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -93,6 +93,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, @@ -1917,7 +1918,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 d2f912ecc..2bdc6c672 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -2063,6 +2063,43 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool .some((p) => p.declarations.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 @@ -2107,7 +2144,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); } @@ -3029,6 +3071,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", @@ -3359,19 +3405,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( @@ -3417,6 +3452,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.nodes.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.nodes) { + 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( @@ -7435,6 +7547,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 @@ -7443,6 +7564,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/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index fc3a378de..39cb565d5 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -32,6 +32,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"; @@ -5022,13 +5023,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.nodes.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.nodes.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.nodes.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)) { @@ -7904,7 +7934,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( @@ -11736,6 +11770,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 5c0e80bda..d3b945f0e 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2162,6 +2162,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 45ed54d1e..3084e15c6 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", @@ -2459,6 +2472,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 ca9dc4d18..b2c82767a 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -554,6 +554,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 028b12c21..102521a09 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -2636,6 +2636,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 870ca14f0..8d2d6bc63 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -49,6 +49,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([ @@ -407,6 +409,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; @@ -512,6 +515,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 bb901a674..54100a413 100644 --- a/packages/compiler/src/native/prepare.ts +++ b/packages/compiler/src/native/prepare.ts @@ -155,6 +155,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 79984d6c5..be07160fd 100644 --- a/packages/compiler/src/native/sanitizer.test.ts +++ b/packages/compiler/src/native/sanitizer.test.ts @@ -104,6 +104,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 ab1c3405e..01d12dedc 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -5676,6 +5676,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/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/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/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/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 5eec9b142..11367335c 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -113,6 +113,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 eab680a55..dacd28937 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -103,6 +103,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 79e6f3d77..4b6b6a6a6 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 @@ -193,6 +194,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 1be81a54e..68b2af8d6 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -137,6 +137,7 @@ typedef enum { SCR_DYNH_CIPHER = 45, /* node:crypto Cipheriv (scr_cipher.c) */ SCR_DYNH_CONSOLE = 32, /* stored global console value */ SCR_DYNH_CONSOLE_INSTANCE = 38, /* node:console Console instance */ + SCR_DYNH_CRYPTO_KEY = 56, /* WebCrypto CryptoKey (scr_webcrypto.c) */ SCR_DYNH_CUSTOM_EVENT = 40, /* ScrEvent with detail — new CustomEvent() */ SCR_DYNH_DATE = 30, /* JavaScript Date with stable native identity */ SCR_DYNH_DECIPHER = 46, /* node:crypto Decipheriv (scr_cipher.c) */ 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..a8a669a57 --- /dev/null +++ b/tests/corpus/6295-jose-hs256/main.mjs @@ -0,0 +1,103 @@ +// 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); + +// 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/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/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/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/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..78fbf2af7 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -97,4 +97,15 @@ 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; +/* 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/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__/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 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..81db57e65 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -210,6 +210,24 @@ 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 + +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/__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/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/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..802fbd3e1 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -839,6 +839,42 @@ 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: 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"), { diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index c22e8a839..fa71b5a08 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -42,6 +42,7 @@ const features: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("native toolchain configuration, staging and helper validation are static", async () => { diff --git a/tests/harness/self-hosting-runtime-pack.test.ts b/tests/harness/self-hosting-runtime-pack.test.ts index 08eb45754..c95d6d9a0 100644 --- a/tests/harness/self-hosting-runtime-pack.test.ts +++ b/tests/harness/self-hosting-runtime-pack.test.ts @@ -43,6 +43,7 @@ const base: NativeLinkFeatures = { tls: false, tlsCa: false, cipher: false, + webcrypto: false, }; test("runtime manifest validation and selection are entirely static", async () => {