Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement RS256 #34

Merged
merged 5 commits into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ process where a web token is represented as the concatenation of
The following signature and MAC algorithms - which are defined in the JSON Web
Algorithms (JWA) [specification](https://www.rfc-editor.org/rfc/rfc7518.html) -
have been implemented already: **HMAC SHA-256** ("HS256"), **HMAC SHA-512**
("HS512") and **none**
("HS512"), **RSASSA-PKCS1-v1_5 SHA-256** ("RS256") and **none**
([_Unsecured JWTs_](https://tools.ietf.org/html/rfc7519#section-6)).
As soon as deno expands its
[crypto library](https://github.com/denoland/deno/tree/master/std/hash), we will
Expand Down
51 changes: 31 additions & 20 deletions create.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { convertUint8ArrayToBase64url } from "./base64/base64url.ts";
import { convertHexToUint8Array, HmacSha256, HmacSha512 } from "./deps.ts";
import { convertHexToUint8Array, HmacSha256, HmacSha512, RSA } from "./deps.ts";

// https://www.rfc-editor.org/rfc/rfc7515.html#page-8
// The payload can be any content and need not be a representation of a JSON object
type Payload = PayloadObject | JsonPrimitive | JsonArray;
type Algorithm = "none" | "HS256" | "HS512";
type Algorithm = "none" | "HS256" | "HS512" | "RS256";
type JsonPrimitive = string | number | boolean | null;
type JsonObject = { [member: string]: JsonValue };
type JsonArray = JsonValue[];
Expand Down Expand Up @@ -33,6 +33,10 @@ interface Jose {
[key: string]: JsonValue | undefined;
}

function assertNever(alg: never, message: string): never {
throw new RangeError(message);
}

// Helper function: setExpiration()
// returns the number of seconds since January 1, 1970, 00:00:00 UTC
function setExpiration(exp: number | Date): number {
Expand All @@ -57,31 +61,43 @@ function makeSigningInput(header: Jose, payload: Payload): string {
}.${convertStringToBase64url(JSON.stringify(payload))}`;
}

function encrypt(alg: Algorithm, key: string, msg: string): string | null {
function assertNever(alg: never): never {
throw new RangeError("no matching crypto algorithm in the header: " + alg);
}
async function encrypt(
alg: Algorithm,
key: string,
msg: string,
): Promise<string> {
switch (alg) {
case "none":
return null;
return "";
case "HS256":
return new HmacSha256(key).update(msg).toString();
case "HS512":
return new HmacSha512(key).update(msg).toString();
case "RS256":
return (
await new RSA(RSA.parseKey(key)).sign(msg, { hash: "sha256" })
).hex();
default:
assertNever(alg);
assertNever(alg, "no matching crypto algorithm in the header: " + alg);
}
}

function makeSignature(alg: Algorithm, key: string, input: string): string {
const encryptionInHex = encrypt(alg, key, input);
return encryptionInHex ? convertHexToBase64url(encryptionInHex) : "";
async function makeSignature(
alg: Algorithm,
key: string,
input: string,
): Promise<string> {
return convertHexToBase64url(await encrypt(alg, key, input));
}

async function makeJwt({ key, header, payload }: JwtInput): Promise<string> {
try {
const signingInput = makeSigningInput(header, payload);
return `${signingInput}.${makeSignature(header.alg, key, signingInput)}`;
return `${signingInput}.${await makeSignature(
header.alg,
key,
signingInput,
)}`;
} catch (err) {
err.message = `Failed to create JWT: ${err.message}`;
throw err;
Expand All @@ -90,17 +106,12 @@ async function makeJwt({ key, header, payload }: JwtInput): Promise<string> {

export {
makeJwt,
encrypt,
setExpiration,
makeSignature,
convertHexToBase64url,
convertStringToBase64url,
assertNever,
};

export type {
Algorithm,
Payload,
PayloadObject,
Jose,
JwtInput,
JsonValue,
};
export type { Algorithm, Payload, PayloadObject, Jose, JwtInput, JsonValue };
1 change: 1 addition & 0 deletions deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export {
} from "https://deno.land/std@0.69.0/encoding/hex.ts";
export { HmacSha256 } from "https://deno.land/std@0.69.0/hash/sha256.ts";
export { HmacSha512 } from "https://deno.land/std@0.69.0/hash/sha512.ts";
export { RSA } from "https://deno.land/x/god_crypto/rsa.ts";
export { addPaddingToBase64url } from "https://deno.land/std@0.69.0/encoding/base64url.ts";
27 changes: 27 additions & 0 deletions examples/private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw
kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr
m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi
NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV
3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2
QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs
kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go
amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM
+bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9
D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC
0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y
lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+
hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp
bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X
+jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B
BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC
2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx
QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz
5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9
Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0
NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j
8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma
3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K
y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB
jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE=
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions examples/public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
MwIDAQAB
-----END PUBLIC KEY-----
30 changes: 30 additions & 0 deletions examples/rsa_example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { serve } from "./example_deps.ts";
import { validateJwt } from "../validate.ts";
import { makeJwt, Jose, Payload } from "../create.ts";

const publicKey = Deno.readTextFileSync("./public.pem");
const privateKey = Deno.readTextFileSync("./private.pem");
const payload: Payload = {
sub: "1234567890",
name: "John Doe",
admin: true,
iat: 1516239022,
};
const header: Jose = {
alg: "RS256",
typ: "JWT",
};

console.log("server is listening at 0.0.0.0:8000");
for await (const req of serve("0.0.0.0:8000")) {
if (req.method === "GET") {
req.respond({
body: (await makeJwt({ header, payload, key: privateKey })) + "\n",
});
} else {
const jwt = new TextDecoder().decode(await Deno.readAll(req.body));
(await validateJwt({ jwt, key: publicKey, algorithm: "RS256" })).isValid
? req.respond({ body: "Valid JWT\n" })
: req.respond({ body: "Invalid JWT\n", status: 401 });
}
}
34 changes: 32 additions & 2 deletions tests/djwt_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
assertThrows,
convertUint8ArrayToHex,
convertHexToUint8Array,
dirname,
fromFileUrl,
} from "./test_deps.ts";

const key = "your-secret";
Expand Down Expand Up @@ -82,11 +84,11 @@ Deno.test("makeSignatureTests", async function (): Promise<void> {
const anotherVerifiedSignatureInBase64Url =
"p2KneqJhji8T0PDlVxcG4DROyzTgWXbDhz_mcTVojXo";
assertEquals(
makeSignature("HS256", "m$y-key", "thisTextWillBeEncrypted"),
await makeSignature("HS256", "m$y-key", "thisTextWillBeEncrypted"),
convertHexToBase64url(computedHmacInHex),
);
assertEquals(
makeSignature(
await makeSignature(
"HS256",
"m$y-key",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
Expand Down Expand Up @@ -379,3 +381,31 @@ Deno.test("makeHmacSha512Test", async function (): Promise<void> {
throw new Error("invalid JWT");
}
});

Deno.test("makeRS256Test", async function (): Promise<void> {
const header = { alg: "RS256" as const, typ: "JWT" };
const payload = {
sub: "1234567890",
name: "John Doe",
admin: true,
iat: 1516239022,
};
const moduleDir = dirname(fromFileUrl(import.meta.url));
const publicKey = Deno.readTextFileSync(moduleDir + "/public.pem");
const privateKey = Deno.readTextFileSync(moduleDir + "/private.pem");
const externallyVerifiedJwt =
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.POstGetfAytaZS82wHcjoTyoqhMyxXiWdR7Nn7A29DNSl0EiXLdwJ6xC6AfgZWF1bOsS_TuYI3OG85AmiExREkrS6tDfTQ2B3WXlrr-wp5AokiRbz3_oB4OxG-W9KcEEbDRcZc0nH3L7LzYptiy1PtAylQGxHTWZXtGz4ht0bAecBgmpdgXMguEIcoqPJ1n3pIWk_dUZegpqx0Lka21H6XxUTxiy8OcaarA8zdnPUnV6AmNP3ecFawIFYdvJB_cm-GvpCSbr8G8y_Mllj8f4x9nBH8pQux89_6gUY618iYv7tuPWBFfEbLxtF2pZS6YC1aSfLQxeNe8djT9YjpvRZA";
const jwt = await makeJwt({ header, payload, key: privateKey });
const validatedJwt = await validateJwt({
jwt,
key: publicKey,
algorithm: "RS256",
});
if (validatedJwt.isValid) {
assertEquals(jwt, externallyVerifiedJwt);
assertEquals(validatedJwt.payload, payload);
assertEquals(validatedJwt.header, header);
} else {
throw new Error("invalid JWT");
}
});
27 changes: 27 additions & 0 deletions tests/private.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAnzyis1ZjfNB0bBgKFMSvvkTtwlvBsaJq7S5wA+kzeVOVpVWw
kWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHcaT92whREFpLv9cj5lTeJSibyr/Mr
m/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIytvHWTxZYEcXLgAXFuUuaS3uF9gEi
NQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0e+lf4s4OxQawWD79J9/5d3Ry0vbV
3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWbV6L11BWkpzGXSW4Hv43qa+GSYOD2
QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9MwIDAQABAoIBACiARq2wkltjtcjs
kFvZ7w1JAORHbEufEO1Eu27zOIlqbgyAcAl7q+/1bip4Z/x1IVES84/yTaM8p0go
amMhvgry/mS8vNi1BN2SAZEnb/7xSxbflb70bX9RHLJqKnp5GZe2jexw+wyXlwaM
+bclUCrh9e1ltH7IvUrRrQnFJfh+is1fRon9Co9Li0GwoN0x0byrrngU8Ak3Y6D9
D8GjQA4Elm94ST3izJv8iCOLSDBmzsPsXfcCUZfmTfZ5DbUDMbMxRnSo3nQeoKGC
0Lj9FkWcfmLcpGlSXTO+Ww1L7EGq+PT3NtRae1FZPwjddQ1/4V905kyQFLamAA5Y
lSpE2wkCgYEAy1OPLQcZt4NQnQzPz2SBJqQN2P5u3vXl+zNVKP8w4eBv0vWuJJF+
hkGNnSxXQrTkvDOIUddSKOzHHgSg4nY6K02ecyT0PPm/UZvtRpWrnBjcEVtHEJNp
bU9pLD5iZ0J9sbzPU/LxPmuAP2Bs8JmTn6aFRspFrP7W0s1Nmk2jsm0CgYEAyH0X
+jpoqxj4efZfkUrg5GbSEhf+dZglf0tTOA5bVg8IYwtmNk/pniLG/zI7c+GlTc9B
BwfMr59EzBq/eFMI7+LgXaVUsM/sS4Ry+yeK6SJx/otIMWtDfqxsLD8CPMCRvecC
2Pip4uSgrl0MOebl9XKp57GoaUWRWRHqwV4Y6h8CgYAZhI4mh4qZtnhKjY4TKDjx
QYufXSdLAi9v3FxmvchDwOgn4L+PRVdMwDNms2bsL0m5uPn104EzM6w1vzz1zwKz
5pTpPI0OjgWN13Tq8+PKvm/4Ga2MjgOgPWQkslulO/oMcXbPwWC3hcRdr9tcQtn9
Imf9n2spL/6EDFId+Hp/7QKBgAqlWdiXsWckdE1Fn91/NGHsc8syKvjjk1onDcw0
NvVi5vcba9oGdElJX3e9mxqUKMrw7msJJv1MX8LWyMQC5L6YNYHDfbPF1q5L4i8j
8mRex97UVokJQRRA452V2vCO6S5ETgpnad36de3MUxHgCOX3qL382Qx9/THVmbma
3YfRAoGAUxL/Eu5yvMK8SAt/dJK6FedngcM3JEFNplmtLYVLWhkIlNRGDwkg3I5K
y18Ae9n7dHVueyslrb6weq7dTkYDi3iOYRW8HRkIQh06wEdbxt0shTzAJvvCQfrB
jg/3747WSsf/zBTcHihTRBdAv6OmdhV4/dD5YBfLAkLrd+mX7iE=
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions tests/public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
MwIDAQAB
-----END PUBLIC KEY-----
1 change: 1 addition & 0 deletions tests/test_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export {
assertEquals,
assertThrows,
} from "https://deno.land/std@0.69.0/testing/asserts.ts";
export { dirname, fromFileUrl } from "https://deno.land/std@0.69.0/path/mod.ts";
62 changes: 46 additions & 16 deletions validate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { makeJwt } from "./create.ts";
import { encrypt, assertNever } from "./create.ts";
import type { Jose, Payload, JsonValue, Algorithm } from "./create.ts";
import { convertBase64urlToUint8Array } from "./base64/base64url.ts";
import { convertUint8ArrayToHex } from "./deps.ts";
import { convertUint8ArrayToHex, convertHexToUint8Array, RSA } from "./deps.ts";

type JwtObject = { header: Jose; payload: Payload; signature: string };
type JwtObjectWithUnknownProps = {
Expand Down Expand Up @@ -165,10 +165,42 @@ function validateAlgorithm(
algorithm: Algorithm | Algorithm[],
jwtAlg: Algorithm,
): boolean {
return (
(Array.isArray(algorithm) && algorithm.includes(jwtAlg)) ||
algorithm === jwtAlg
);
if (Array.isArray(algorithm)) {
if (algorithm.length > 1 && algorithm.includes("none")) {
throw Error("algorithm 'none' must be used alone");
} else return algorithm.includes(jwtAlg);
} else {
return algorithm === jwtAlg;
}
}

async function verifySignature({
signature,
key,
alg,
signingInput,
}: {
signature: string;
key: string;
alg: Algorithm;
signingInput: string;
}): Promise<boolean> {
switch (alg) {
case "none":
case "HS256":
case "HS512": {
return signature === (await encrypt(alg, key, signingInput));
}
case "RS256": {
return await new RSA(RSA.parseKey(key)).verify(
convertHexToUint8Array(signature),
signingInput,
{ hash: "sha256" },
);
}
default:
assertNever(alg, "no matching crypto alg in the header: " + alg);
}
}

async function validateJwt({
Expand All @@ -186,8 +218,12 @@ async function validateJwt({
throw Error("no matching algorithm: " + oldJwtObject.header.alg);
}
if (
oldJwtObject.signature !==
parseAndDecode(await makeJwt({ ...oldJwtObject, key })).signature
!(await verifySignature({
signature: oldJwtObject.signature,
key,
alg: oldJwtObject.header.alg,
signingInput: jwt.slice(0, jwt.lastIndexOf(".")),
}))
) {
throw Error("signatures don't match");
}
Expand All @@ -205,18 +241,12 @@ async function validateJwt({
export {
validateJwt,
validateJwtObject,
verifySignature,
checkHeaderCrit,
parseAndDecode,
isExpired,
isObject,
hasProperty,
};

export type {
Jose,
Payload,
Handlers,
JwtObject,
JwtValidation,
Validation,
};
export type { Jose, Payload, Handlers, JwtObject, JwtValidation, Validation };