203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
const { EOL } = require('os')
|
|
|
|
const base64url = require('../help/base64url')
|
|
const isDisjoint = require('../help/is_disjoint')
|
|
const isObject = require('../help/is_object')
|
|
let validateCrit = require('../help/validate_crit')
|
|
const getKey = require('../help/get_key')
|
|
const { KeyStore } = require('../jwks')
|
|
const errors = require('../errors')
|
|
const { check, verify } = require('../jwa')
|
|
const JWK = require('../jwk')
|
|
|
|
const { detect: resolveSerialization } = require('./serializers')
|
|
|
|
validateCrit = validateCrit.bind(undefined, errors.JWSInvalid)
|
|
const SINGLE_RECIPIENT = new Set(['compact', 'flattened', 'preparsed'])
|
|
|
|
/*
|
|
* @public
|
|
*/
|
|
const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], complete = false, algorithms } = {}) => {
|
|
key = getKey(key, true)
|
|
|
|
if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) {
|
|
throw new TypeError('"algorithms" option must be an array of non-empty strings')
|
|
} else if (algorithms) {
|
|
algorithms = new Set(algorithms)
|
|
}
|
|
|
|
if (!Array.isArray(crit) || crit.some(s => typeof s !== 'string' || !s)) {
|
|
throw new TypeError('"crit" option must be an array of non-empty strings')
|
|
}
|
|
|
|
if (!serialization) {
|
|
serialization = resolveSerialization(jws)
|
|
}
|
|
|
|
let prot // protected header
|
|
let header // unprotected header
|
|
let payload
|
|
let signature
|
|
let alg
|
|
|
|
// treat general format with one recipient as flattened
|
|
// skips iteration and avoids multi errors in this case
|
|
if (serialization === 'general' && jws.signatures.length === 1) {
|
|
serialization = 'flattened'
|
|
const { signatures, ...root } = jws
|
|
jws = { ...root, ...signatures[0] }
|
|
}
|
|
|
|
let decoded
|
|
|
|
if (SINGLE_RECIPIENT.has(serialization)) {
|
|
let parsedProt = {}
|
|
|
|
switch (serialization) {
|
|
case 'compact': // compact serialization format
|
|
([prot, payload, signature] = jws.split('.'))
|
|
break
|
|
case 'flattened': // flattened serialization format
|
|
({ protected: prot, payload, signature, header } = jws)
|
|
break
|
|
case 'preparsed': { // from the JWT module
|
|
({ decoded } = jws);
|
|
([prot, payload, signature] = jws.token.split('.'))
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!header) {
|
|
skipDisjointCheck = true
|
|
}
|
|
|
|
if (decoded) {
|
|
parsedProt = decoded.header
|
|
} else if (prot) {
|
|
try {
|
|
parsedProt = base64url.JSON.decode(prot)
|
|
} catch (err) {
|
|
throw new errors.JWSInvalid('could not parse JWS protected header')
|
|
}
|
|
} else {
|
|
skipDisjointCheck = skipDisjointCheck || true
|
|
}
|
|
|
|
if (!skipDisjointCheck && !isDisjoint(parsedProt, header)) {
|
|
throw new errors.JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint')
|
|
}
|
|
|
|
const combinedHeader = { ...parsedProt, ...header }
|
|
validateCrit(parsedProt, header, crit)
|
|
|
|
alg = parsedProt.alg || (header && header.alg)
|
|
if (!alg) {
|
|
throw new errors.JWSInvalid('missing JWS signature algorithm')
|
|
} else if (algorithms && !algorithms.has(alg)) {
|
|
throw new errors.JOSEAlgNotWhitelisted('alg not whitelisted')
|
|
}
|
|
|
|
if (key instanceof KeyStore) {
|
|
const keystore = key
|
|
const keys = keystore.all({ kid: combinedHeader.kid, alg: combinedHeader.alg, key_ops: ['verify'] })
|
|
switch (keys.length) {
|
|
case 0:
|
|
throw new errors.JWKSNoMatchingKey()
|
|
case 1:
|
|
// treat the call as if a Key instance was passed in
|
|
// skips iteration and avoids multi errors in this case
|
|
key = keys[0]
|
|
break
|
|
default: {
|
|
const errs = []
|
|
for (const key of keys) {
|
|
try {
|
|
return jwsVerify(true, serialization, jws, key, { crit, complete, algorithms: algorithms ? [...algorithms] : undefined })
|
|
} catch (err) {
|
|
errs.push(err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
const multi = new errors.JOSEMultiError(errs)
|
|
if ([...multi].some(e => e instanceof errors.JWSVerificationFailed)) {
|
|
throw new errors.JWSVerificationFailed()
|
|
}
|
|
throw multi
|
|
}
|
|
}
|
|
}
|
|
|
|
if (key === JWK.EmbeddedJWK) {
|
|
if (!isObject(combinedHeader.jwk)) {
|
|
throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a JSON object')
|
|
}
|
|
key = JWK.asKey(combinedHeader.jwk)
|
|
if (key.type !== 'public') {
|
|
throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a public key')
|
|
}
|
|
} else if (key === JWK.EmbeddedX5C) {
|
|
if (!Array.isArray(combinedHeader.x5c) || !combinedHeader.x5c.length || combinedHeader.x5c.some(c => typeof c !== 'string' || !c)) {
|
|
throw new errors.JWSInvalid('JWS Header Parameter "x5c" must be a JSON array of certificate value strings')
|
|
}
|
|
key = JWK.asKey(
|
|
`-----BEGIN CERTIFICATE-----${EOL}${(combinedHeader.x5c[0].match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`,
|
|
{ x5c: combinedHeader.x5c }
|
|
)
|
|
}
|
|
|
|
check(key, 'verify', alg)
|
|
|
|
const toBeVerified = Buffer.concat([
|
|
Buffer.from(prot || ''),
|
|
Buffer.from('.'),
|
|
Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
|
|
])
|
|
|
|
if (!verify(alg, key, toBeVerified, base64url.decodeToBuffer(signature))) {
|
|
throw new errors.JWSVerificationFailed()
|
|
}
|
|
|
|
if (combinedHeader.b64 === false) {
|
|
payload = Buffer.from(payload)
|
|
} else {
|
|
payload = base64url.decodeToBuffer(payload)
|
|
}
|
|
|
|
if (complete) {
|
|
const result = { payload, key }
|
|
if (prot) result.protected = parsedProt
|
|
if (header) result.header = header
|
|
return result
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
// general serialization format
|
|
const { signatures, ...root } = jws
|
|
const errs = []
|
|
for (const recipient of signatures) {
|
|
try {
|
|
return jwsVerify(false, 'flattened', { ...root, ...recipient }, key, { crit, complete, algorithms: algorithms ? [...algorithms] : undefined })
|
|
} catch (err) {
|
|
errs.push(err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
const multi = new errors.JOSEMultiError(errs)
|
|
if ([...multi].some(e => e instanceof errors.JWSVerificationFailed)) {
|
|
throw new errors.JWSVerificationFailed()
|
|
} else if ([...multi].every(e => e instanceof errors.JWKSNoMatchingKey)) {
|
|
throw new errors.JWKSNoMatchingKey()
|
|
}
|
|
throw multi
|
|
}
|
|
|
|
module.exports = {
|
|
bare: jwsVerify,
|
|
verify: jwsVerify.bind(undefined, false, undefined)
|
|
}
|