diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec923c6..aa54552 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,5 +9,5 @@ jobs: - uses: actions/checkout@master - uses: denolib/setup-deno@master with: - deno-version: v1.6.0 + deno-version: v1.7.0 - run: deno test -A diff --git a/.gitignore b/.gitignore deleted file mode 100644 index db054f7..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -egg.json diff --git a/README.md b/README.md index a024a15..c8874bf 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ The following signature and MAC algorithms have been implemented: - HS256 (HMAC SHA-256) - HS512 (HMAC SHA-512) - RS256 (RSASSA-PKCS1-v1_5 SHA-256) +- RS512 (RSASSA-PKCS1-v1_5 SHA-512) +- PS256 (rsassa-pss SHA-256) +- PS512 (rsassa-pss SHA-512) - none ([_Unsecured JWTs_](https://tools.ietf.org/html/rfc7519#section-6)). ## Serialization diff --git a/algorithm.ts b/algorithm.ts index 8f0e924..44aef5f 100644 --- a/algorithm.ts +++ b/algorithm.ts @@ -3,7 +3,15 @@ * are described in the separate JSON Web Algorithms (JWA) specification: * https://www.rfc-editor.org/rfc/rfc7518 */ -export type Algorithm = "none" | "HS256" | "HS512" | "RS256"; +export type Algorithm = + | "none" + | "HS256" + | "HS512" + | "RS256" + | "RS512" + | "PS256" + | "PS512"; + export type AlgorithmInput = Algorithm | Array>; export function verify(algorithm: AlgorithmInput, jwtAlg: string): boolean { diff --git a/deps.ts b/deps.ts index bca1dee..8846c9c 100644 --- a/deps.ts +++ b/deps.ts @@ -1,8 +1,8 @@ -export * as base64url from "https://deno.land/std@0.80.0/encoding/base64url.ts"; +export * as base64url from "https://deno.land/std@0.84.0/encoding/base64url.ts"; export { decodeString as convertHexToUint8Array, encodeToString as convertUint8ArrayToHex, -} from "https://deno.land/std@0.80.0/encoding/hex.ts"; -export { HmacSha256 } from "https://deno.land/std@0.80.0/hash/sha256.ts"; -export { HmacSha512 } from "https://deno.land/std@0.80.0/hash/sha512.ts"; -export { RSA } from "https://deno.land/x/god_crypto@v1.4.6/rsa.ts"; +} from "https://deno.land/std@0.84.0/encoding/hex.ts"; +export { HmacSha256 } from "https://deno.land/std@0.84.0/hash/sha256.ts"; +export { HmacSha512 } from "https://deno.land/std@0.84.0/hash/sha512.ts"; +export { RSA } from "https://deno.land/x/god_crypto@v1.4.8/rsa.ts"; diff --git a/examples/example_deps.ts b/examples/example_deps.ts index 511bdf8..8ffb07f 100644 --- a/examples/example_deps.ts +++ b/examples/example_deps.ts @@ -1,3 +1,3 @@ -export { serve } from "https://deno.land/std@0.80.0/http/server.ts"; -export { decode, encode } from "https://deno.land/std@0.80.0/encoding/utf8.ts"; -export { dirname, fromFileUrl } from "https://deno.land/std@0.80.0/path/mod.ts"; +export { serve } from "https://deno.land/std@0.84.0/http/server.ts"; +export { decode, encode } from "https://deno.land/std@0.84.0/encoding/utf8.ts"; +export { dirname, fromFileUrl } from "https://deno.land/std@0.84.0/path/mod.ts"; diff --git a/signature.ts b/signature.ts index 008e29e..8eee8bb 100644 --- a/signature.ts +++ b/signature.ts @@ -53,7 +53,31 @@ async function encrypt( return new HmacSha512(key).update(message).toString(); case "RS256": return ( - await new RSA(RSA.parseKey(key)).sign(message, { hash: "sha256" }) + await new RSA(RSA.parseKey(key)).sign( + message, + { algorithm: "rsassa-pkcs1-v1_5", hash: "sha256" }, + ) + ).hex(); + case "RS512": + return ( + await new RSA(RSA.parseKey(key)).sign( + message, + { algorithm: "rsassa-pkcs1-v1_5", hash: "sha512" }, + ) + ).hex(); + case "PS256": + return ( + await new RSA(RSA.parseKey(key)).sign( + message, + { algorithm: "rsassa-pss", hash: "sha256" }, + ) + ).hex(); + case "PS512": + return ( + await new RSA(RSA.parseKey(key)).sign( + message, + { algorithm: "rsassa-pss", hash: "sha512" }, + ) ).hex(); default: assertNever( @@ -82,26 +106,53 @@ export async function verify({ algorithm: Algorithm; signingInput: string; }): Promise { - switch (algorithm) { - case "none": - case "HS256": - case "HS512": { - return safeCompare( - signature, - (await encrypt(algorithm, key, signingInput)), - ); - } - case "RS256": { - return await new RSA(RSA.parseKey(key)).verify( - convertHexToUint8Array(signature), - signingInput, - { hash: "sha256" }, - ); + // Need to add a try...catch statement because the god_crypto library throws + // strings instead of Error objects. + try { + switch (algorithm) { + case "none": + case "HS256": + case "HS512": { + return safeCompare( + signature, + (await encrypt(algorithm, key, signingInput)), + ); + } + case "RS256": { + return await new RSA(RSA.parseKey(key)).verify( + convertHexToUint8Array(signature), + signingInput, + { algorithm: "rsassa-pkcs1-v1_5", hash: "sha256" }, + ); + } + case "RS512": { + return await new RSA(RSA.parseKey(key)).verify( + convertHexToUint8Array(signature), + signingInput, + { algorithm: "rsassa-pkcs1-v1_5", hash: "sha512" }, + ); + } + case "PS256": { + return await new RSA(RSA.parseKey(key)).verify( + convertHexToUint8Array(signature), + signingInput, + { algorithm: "rsassa-pss", hash: "sha256" }, + ); + } + case "PS512": { + return await new RSA(RSA.parseKey(key)).verify( + convertHexToUint8Array(signature), + signingInput, + { algorithm: "rsassa-pss", hash: "sha512" }, + ); + } + default: + assertNever( + algorithm, + "no matching crypto algorithm in the header: " + algorithm, + ); } - default: - assertNever( - algorithm, - "no matching crypto algorithm in the header: " + algorithm, - ); + } catch (err) { + throw err instanceof Error ? err : new Error(err); } } diff --git a/tests/test.ts b/tests/test.ts index 6abb3bb..f171be7 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -395,6 +395,15 @@ Deno.test({ Error, `The jwt's algorithm does not match the specified algorithm 'HS256'.`, ); + assertThrowsAsync( + async () => { + const jwtWithInvalidSignature = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzcXNrz0ogthfEd2o"; + await verify(jwtWithInvalidSignature, key, "HS256"); + }, + Error, + "The jwt's signature does not match the verification signature.", + ); }, }); @@ -435,6 +444,105 @@ Deno.test("[jwt] RS256 algorithm", async function (): Promise { ); assertEquals(jwt, externallyVerifiedJwt); assertEquals(receivedPayload, payload); + + assertThrowsAsync( + async () => { + const jwtWithInvalidSignature = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtlQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA"; + const receivedPayload2 = await verify( + jwtWithInvalidSignature, + publicKey, + "RS256", + ); + }, + Error, + `Decryption error`, + ); +}); + +Deno.test("[jwt] RS512 algorithm", async function (): Promise { + const header = { alg: "RS512" as const, typ: "JWT" }; + const payload = { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }; + const moduleDir = dirname(fromFileUrl(import.meta.url)); + const publicKey = await Deno.readTextFile(moduleDir + "/certs/public.pem"); + const privateKey = await Deno.readTextFile(moduleDir + "/certs/private.pem"); + const externallyVerifiedJwt = + "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.JlX3gXGyClTBFciHhknWrjo7SKqyJ5iBO0n-3S2_I7cIgfaZAeRDJ3SQEbaPxVC7X8aqGCOM-pQOjZPKUJN8DMFrlHTOdqMs0TwQ2PRBmVAxXTSOZOoEhD4ZNCHohYoyfoDhJDP4Qye_FCqu6POJzg0Jcun4d3KW04QTiGxv2PkYqmB7nHxYuJdnqE3704hIS56pc_8q6AW0WIT0W-nIvwzaSbtBU9RgaC7ZpBD2LiNE265UBIFraMDF8IAFw9itZSUCTKg1Q-q27NwwBZNGYStMdIBDor2Bsq5ge51EkWajzZ7ALisVp-bskzUsqUf77ejqX_CBAqkNdH1Zebn93A"; + const jwt = await create(header, payload, privateKey); + const receivedPayload = await verify( + jwt, + publicKey, + "RS512", + ); + assertEquals(jwt, externallyVerifiedJwt); + assertEquals(receivedPayload, payload); +}); + +Deno.test("[jwt] PS256 algorithm", async function (): Promise { + const header = { alg: "PS256" as const, typ: "JWT" }; + const payload = { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }; + const moduleDir = dirname(fromFileUrl(import.meta.url)); + const publicKey = await Deno.readTextFile(moduleDir + "/certs/public.pem"); + const privateKey = await Deno.readTextFile(moduleDir + "/certs/private.pem"); + const externallyVerifiedJwt = + "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.hZnl5amPk_I3tb4O-Otci_5XZdVWhPlFyVRvcqSwnDo_srcysDvhhKOD01DigPK1lJvTSTolyUgKGtpLqMfRDXQlekRsF4XhAjYZTmcynf-C-6wO5EI4wYewLNKFGGJzHAknMgotJFjDi_NCVSjHsW3a10nTao1lB82FRS305T226Q0VqNVJVWhE4G0JQvi2TssRtCxYTqzXVt22iDKkXeZJARZ1paXHGV5Kd1CljcZtkNZYIGcwnj65gvuCwohbkIxAnhZMJXCLaVvHqv9l-AAUV7esZvkQR1IpwBAiDQJh4qxPjFGylyXrHMqh5NlT_pWL2ZoULWTg_TJjMO9TuQ"; + const jwt = await create(header, payload, privateKey); + const receivedPayload = await verify( + jwt, + publicKey, + "PS256", + ); + const receivedPayloadFromExternalJwt = await verify( + externallyVerifiedJwt, + publicKey, + "PS256", + ); + assertEquals(receivedPayload, payload); + assertEquals(receivedPayloadFromExternalJwt, payload); + + assertThrowsAsync( + async () => { + const jwtWithInvalidSignature = + "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.hZnl5amPk_I3tb4O-Otci_5XZdVWhPlFyVRvcqSwnDo_srcysDvhhKOD01DigPK1lJvTSTolyUgKGtpLqMfRDXQlekRsF4XhAjYZTmcynf-C-6wO5EI4wYewLNKFGGJzHAknMgotJFjDi_NCVSjHsW3a10nTao1lB82FRS305T226Q0VqNVJVWhE4G0JQvi2TssRtCzXVt22iDKkXeZJARZ1paXHGV5Kd1CljcZtkNZYIGcwnj65gvuCwohbkIxAnhZMJXCLaVvHqv9l-AAUV7esZvkQR1IpwBAiDQJh4qxPjFGylyXrHMqh5NlT_pWL2ZoULWTg_TJjMO9TuQ"; + const receivedPayload2 = await verify( + jwtWithInvalidSignature, + publicKey, + "PS256", + ); + }, + Error, + `The jwt's signature does not match the verification signature.`, + ); +}); + +Deno.test("[jwt] PS512 algorithm", async function (): Promise { + const header = { alg: "PS512" as const, typ: "JWT" }; + const payload = { + sub: "1234567890", + name: "John Doe", + admin: true, + iat: 1516239022, + }; + const moduleDir = dirname(fromFileUrl(import.meta.url)); + const publicKey = await Deno.readTextFile(moduleDir + "/certs/public.pem"); + const privateKey = await Deno.readTextFile(moduleDir + "/certs/private.pem"); + const jwt = await create(header, payload, privateKey); + const receivedPayload = await verify( + jwt, + publicKey, + "PS512", + ); + assertEquals(receivedPayload, payload); }); Deno.test("[jwt] getNumericDate", function (): void { diff --git a/tests/test_deps.ts b/tests/test_deps.ts index 60c2d4e..8ce7cae 100644 --- a/tests/test_deps.ts +++ b/tests/test_deps.ts @@ -2,6 +2,6 @@ export { assertEquals, assertThrows, assertThrowsAsync, -} from "https://deno.land/std@0.80.0/testing/asserts.ts"; +} from "https://deno.land/std@0.84.0/testing/asserts.ts"; -export { dirname, fromFileUrl } from "https://deno.land/std@0.80.0/path/mod.ts"; +export { dirname, fromFileUrl } from "https://deno.land/std@0.84.0/path/mod.ts";