Last active
October 14, 2024 21:44
-
-
Save NetOpWibby/73d7bf536a395de82a4811fb5f119144 to your computer and use it in GitHub Desktop.
Fixes "prime256v1" issue with Apple-supplied private keys (`jsonwebtoken` isn't expecting "p256"). In `sign.ts` I removed all the validation checks and `lodash` crap. I'm using Deno. Oh, and you should extract the `timespan` and `validateAsymmetricKey` functions from `jsonwebtoken`. Maybe I'll do that someday and upload a module to jsr.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// native | |
import { Buffer } from "node:buffer"; | |
import { KeyObject, createSecretKey, createPrivateKey } from "node:crypto"; | |
/// import | |
import jws from "npm:[email protected]"; | |
/// util | |
import timespan from "./lib/timespan.ts"; | |
import validateAsymmetricKey from "./lib/validate-asymmetric-key.ts"; | |
const OPTIONS_TO_PAYLOAD = { | |
"audience": "aud", | |
"issuer": "iss", | |
"jwtid": "jti", | |
"subject": "sub" | |
}; | |
const OPTIONS_FOR_OBJECTS = [ | |
"audience", | |
"expiresIn", | |
"issuer", | |
"jwtid", | |
"notBefore", | |
"noTimestamp", | |
"subject" | |
]; | |
const SUPPORTED_ALGS = [ | |
"RS256", "RS384", "RS512", | |
"PS256", "PS384", "PS512", | |
"ES256", "ES384", "ES512", | |
"HS256", "HS384", "HS512", | |
"none" | |
]; | |
/// export | |
export default (payload, secretOrPrivateKey, options, callback) => { | |
if (typeof options === "function") { | |
callback = options; | |
options = {}; | |
} else { | |
options = options || {}; | |
} | |
const isObjectPayload = typeof payload === "object" && !Buffer.isBuffer(payload); | |
const header = Object.assign({ | |
alg: options.algorithm || "HS256", | |
kid: options.keyid, | |
typ: isObjectPayload ? "JWT" : undefined | |
}, options.header); | |
function failure(err) { | |
if (callback) | |
return callback(err); | |
throw err; | |
} | |
if (!secretOrPrivateKey && options.algorithm !== "none") | |
return failure(new Error("secretOrPrivateKey must have a value")); | |
if (secretOrPrivateKey !== null && !(secretOrPrivateKey instanceof KeyObject)) { | |
try { | |
secretOrPrivateKey = createPrivateKey(secretOrPrivateKey) | |
} catch(_) { | |
try { | |
secretOrPrivateKey = createSecretKey( | |
typeof secretOrPrivateKey === "string" ? | |
Buffer.from(secretOrPrivateKey) : | |
secretOrPrivateKey | |
) | |
} catch(_) { | |
return failure(new Error("secretOrPrivateKey is not valid key material")); | |
} | |
} | |
} | |
if (header.alg.startsWith("HS") && secretOrPrivateKey.type !== "secret") { | |
return failure(new Error((`secretOrPrivateKey must be a symmetric key when using ${header.alg}`))); | |
} else if (/^(?:RS|PS|ES)/.test(header.alg)) { | |
if (secretOrPrivateKey.type !== "private") | |
return failure(new Error((`secretOrPrivateKey must be an asymmetric key when using ${header.alg}`))); | |
if ( | |
!options.allowInsecureKeySizes && | |
!header.alg.startsWith("ES") && | |
secretOrPrivateKey.asymmetricKeyDetails !== undefined && // KeyObject.asymmetricKeyDetails is supported in Node 15+ | |
secretOrPrivateKey.asymmetricKeyDetails.modulusLength < 2048 | |
) return failure(new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`)); | |
} | |
const invalid_options = OPTIONS_FOR_OBJECTS.filter(opt => typeof options[opt] !== "undefined"); | |
if (invalid_options.length > 0) | |
return failure(new Error(`invalid ${invalid_options.join(",")} option for ${(typeof payload )} payload`)); | |
if (typeof payload.exp !== "undefined" && typeof options.expiresIn !== "undefined") | |
return failure(new Error(`Bad "options.expiresIn" option the payload already has an "exp" property.`)); | |
if (typeof payload.nbf !== "undefined" && typeof options.notBefore !== "undefined") | |
return failure(new Error(`Bad "options.notBefore" option the payload already has an "nbf" property.`)); | |
if (!options.allowInvalidAsymmetricKeyTypes) { | |
try { | |
validateAsymmetricKey(header.alg, secretOrPrivateKey); | |
} catch(error) { | |
return failure(error); | |
} | |
} | |
const timestamp = payload.iat || Math.floor(Date.now() / 1000); | |
if (options.noTimestamp) | |
delete payload.iat; | |
else if (isObjectPayload) | |
payload.iat = timestamp; | |
if (typeof options.notBefore !== "undefined") { | |
try { | |
payload.nbf = timespan(options.notBefore, timestamp); | |
} catch(err) { | |
return failure(err); | |
} | |
if (typeof payload.nbf === "undefined") | |
return failure(new Error(`"notBefore" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60.`)); | |
} | |
if (typeof options.expiresIn !== "undefined" && typeof payload === "object") { | |
try { | |
payload.exp = timespan(options.expiresIn, timestamp); | |
} catch(err) { | |
return failure(err); | |
} | |
if (typeof payload.exp === "undefined") | |
return failure(new Error(`"expiresIn" should be a number of seconds or string representing a timespan eg: "1d", "20h", 60.`)); | |
} | |
Object.keys(OPTIONS_TO_PAYLOAD).forEach(key => { | |
const claim = OPTIONS_TO_PAYLOAD[key]; | |
if (typeof options[key] !== "undefined") { | |
if (typeof payload[claim] !== "undefined") | |
return failure(new Error(`Bad "options.${key}" option. The payload already has an "${claim}" property.`)); | |
payload[claim] = options[key]; | |
} | |
}); | |
const encoding = options.encoding || "utf8"; | |
const signature = jws.sign({ encoding, header, payload, secret: secretOrPrivateKey }); | |
// TODO: Remove in favor of the modulus length check before signing once node 15+ is the minimum supported version | |
if (!options.allowInsecureKeySizes && /^(?:RS|PS)/.test(header.alg) && signature.length < 256) | |
throw new Error(`secretOrPrivateKey has a minimum key size of 2048 bits for ${header.alg}`) | |
return signature; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// util | |
const allowedAlgorithmsForKeys = { | |
"ec": ["ES256", "ES384", "ES512"], | |
"rsa": ["RS256", "PS256", "RS384", "PS384", "RS512", "PS512"], | |
"rsa-pss": ["PS256", "PS384", "PS512"] | |
}; | |
const allowedCurves = { | |
ES256: ["prime256v1", "p256"], | |
ES384: ["secp384r1", "s384"], | |
ES512: ["secp521r1", "s521"] | |
}; | |
/// export | |
export default (algorithm, key) => { | |
if (!algorithm || !key) | |
return; | |
const keyType = key.asymmetricKeyType; | |
if (!keyType) | |
return; | |
const allowedAlgorithms = allowedAlgorithmsForKeys[keyType]; | |
if (!allowedAlgorithms) | |
throw new Error(`Unknown key type "${keyType}".`); | |
if (!allowedAlgorithms.includes(algorithm)) | |
throw new Error(`"alg" parameter for "${keyType}" key type must be one of: ${allowedAlgorithms.join(", ")}.`) | |
switch(keyType) { | |
case "ec": { | |
const keyCurve = key.asymmetricKeyDetails.namedCurve; | |
const allowedCurve = allowedCurves[algorithm]; | |
if (!allowedCurve.includes(keyCurve)) | |
throw new Error(`"alg" parameter "${algorithm}" requires curve "${allowedCurve}".`); | |
break; | |
} | |
case "rsa-pss": { | |
const length = parseInt(algorithm.slice(-3), 10); | |
const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails; | |
if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) | |
throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`); | |
if (saltLength !== undefined && saltLength > length >> 3) | |
throw new Error(`Invalid key for this operation, its RSA-PSS parameter saltLength does not meet the requirements of "alg" ${algorithm}.`) | |
break; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment