UNPKG

6.7 kBJavaScriptView Raw
1const { EOL } = require('os')
2
3const base64url = require('../help/base64url')
4const isDisjoint = require('../help/is_disjoint')
5const isObject = require('../help/is_object')
6let validateCrit = require('../help/validate_crit')
7const getKey = require('../help/get_key')
8const { KeyStore } = require('../jwks')
9const errors = require('../errors')
10const { check, verify } = require('../jwa')
11const JWK = require('../jwk')
12
13const { detect: resolveSerialization } = require('./serializers')
14
15validateCrit = validateCrit.bind(undefined, errors.JWSInvalid)
16const SINGLE_RECIPIENT = new Set(['compact', 'flattened', 'preparsed'])
17
18/*
19 * @public
20 */
21const jwsVerify = (skipDisjointCheck, serialization, jws, key, { crit = [], complete = false, algorithms, parse = true, encoding = 'utf8' } = {}) => {
22 key = getKey(key, true)
23
24 if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) {
25 throw new TypeError('"algorithms" option must be an array of non-empty strings')
26 } else if (algorithms) {
27 algorithms = new Set(algorithms)
28 }
29
30 if (!Array.isArray(crit) || crit.some(s => typeof s !== 'string' || !s)) {
31 throw new TypeError('"crit" option must be an array of non-empty strings')
32 }
33
34 if (!serialization) {
35 serialization = resolveSerialization(jws)
36 }
37
38 let prot // protected header
39 let header // unprotected header
40 let payload
41 let signature
42 let alg
43
44 // treat general format with one recipient as flattened
45 // skips iteration and avoids multi errors in this case
46 if (serialization === 'general' && jws.signatures.length === 1) {
47 serialization = 'flattened'
48 const { signatures, ...root } = jws
49 jws = { ...root, ...signatures[0] }
50 }
51
52 let decoded
53
54 if (SINGLE_RECIPIENT.has(serialization)) {
55 let parsedProt = {}
56
57 switch (serialization) {
58 case 'compact': // compact serialization format
59 ([prot, payload, signature] = jws.split('.'))
60 break
61 case 'flattened': // flattened serialization format
62 ({ protected: prot, payload, signature, header } = jws)
63 break
64 case 'preparsed': { // from the JWT module
65 ({ decoded } = jws);
66 ([prot, payload, signature] = jws.token.split('.'))
67 break
68 }
69 }
70
71 if (!header) {
72 skipDisjointCheck = true
73 }
74
75 if (decoded) {
76 parsedProt = decoded.header
77 } else if (prot) {
78 try {
79 parsedProt = base64url.JSON.decode(prot)
80 } catch (err) {
81 throw new errors.JWSInvalid('could not parse JWS protected header')
82 }
83 } else {
84 skipDisjointCheck = skipDisjointCheck || true
85 }
86
87 if (!skipDisjointCheck && !isDisjoint(parsedProt, header)) {
88 throw new errors.JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint')
89 }
90
91 const combinedHeader = { ...parsedProt, ...header }
92 validateCrit(parsedProt, header, crit)
93
94 alg = parsedProt.alg || (header && header.alg)
95 if (!alg) {
96 throw new errors.JWSInvalid('missing JWS signature algorithm')
97 } else if (algorithms && !algorithms.has(alg)) {
98 throw new errors.JOSEAlgNotWhitelisted('alg not whitelisted')
99 }
100
101 if (key instanceof KeyStore) {
102 const keystore = key
103 const keys = keystore.all({ kid: combinedHeader.kid, alg: combinedHeader.alg, key_ops: ['verify'] })
104 switch (keys.length) {
105 case 0:
106 throw new errors.JWKSNoMatchingKey()
107 case 1:
108 // treat the call as if a Key instance was passed in
109 // skips iteration and avoids multi errors in this case
110 key = keys[0]
111 break
112 default: {
113 const errs = []
114 for (const key of keys) {
115 try {
116 return jwsVerify(true, serialization, jws, key, { crit, complete, encoding, parse, algorithms: algorithms ? [...algorithms] : undefined })
117 } catch (err) {
118 errs.push(err)
119 continue
120 }
121 }
122
123 const multi = new errors.JOSEMultiError(errs)
124 if ([...multi].some(e => e instanceof errors.JWSVerificationFailed)) {
125 throw new errors.JWSVerificationFailed()
126 }
127 throw multi
128 }
129 }
130 }
131
132 if (key === JWK.EmbeddedJWK) {
133 if (!isObject(combinedHeader.jwk)) {
134 throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a JSON object')
135 }
136 key = JWK.asKey(combinedHeader.jwk)
137 if (key.type !== 'public') {
138 throw new errors.JWSInvalid('JWS Header Parameter "jwk" must be a public key')
139 }
140 } else if (key === JWK.EmbeddedX5C) {
141 if (!Array.isArray(combinedHeader.x5c) || !combinedHeader.x5c.length || combinedHeader.x5c.some(c => typeof c !== 'string' || !c)) {
142 throw new errors.JWSInvalid('JWS Header Parameter "x5c" must be a JSON array of certificate value strings')
143 }
144 key = JWK.asKey(
145 `-----BEGIN CERTIFICATE-----${EOL}${(combinedHeader.x5c[0].match(/.{1,64}/g) || []).join(EOL)}${EOL}-----END CERTIFICATE-----`,
146 { x5c: combinedHeader.x5c }
147 )
148 }
149
150 check(key, 'verify', alg)
151
152 const toBeVerified = Buffer.concat([
153 Buffer.from(prot || ''),
154 Buffer.from('.'),
155 Buffer.isBuffer(payload) ? payload : Buffer.from(payload)
156 ])
157
158 if (!verify(alg, key, toBeVerified, base64url.decodeToBuffer(signature))) {
159 throw new errors.JWSVerificationFailed()
160 }
161
162 if (!combinedHeader.crit || !combinedHeader.crit.includes('b64') || combinedHeader.b64) {
163 if (parse) {
164 payload = decoded ? decoded.payload : base64url.JSON.decode.try(payload, encoding)
165 } else {
166 payload = base64url.decodeToBuffer(payload)
167 }
168 }
169
170 if (complete) {
171 const result = { payload, key }
172 if (prot) result.protected = parsedProt
173 if (header) result.header = header
174 return result
175 }
176
177 return payload
178 }
179
180 // general serialization format
181 const { signatures, ...root } = jws
182 const errs = []
183 for (const recipient of signatures) {
184 try {
185 return jwsVerify(false, 'flattened', { ...root, ...recipient }, key, { crit, complete, encoding, parse, algorithms: algorithms ? [...algorithms] : undefined })
186 } catch (err) {
187 errs.push(err)
188 continue
189 }
190 }
191
192 const multi = new errors.JOSEMultiError(errs)
193 if ([...multi].some(e => e instanceof errors.JWSVerificationFailed)) {
194 throw new errors.JWSVerificationFailed()
195 } else if ([...multi].every(e => e instanceof errors.JWKSNoMatchingKey)) {
196 throw new errors.JWKSNoMatchingKey()
197 }
198 throw multi
199}
200
201module.exports = {
202 bare: jwsVerify,
203 verify: jwsVerify.bind(undefined, false, undefined)
204}