1 | const { EOL } = require('os')
|
2 |
|
3 | const base64url = require('../help/base64url')
|
4 | const isDisjoint = require('../help/is_disjoint')
|
5 | const isObject = require('../help/is_object')
|
6 | let validateCrit = require('../help/validate_crit')
|
7 | const getKey = require('../help/get_key')
|
8 | const { KeyStore } = require('../jwks')
|
9 | const errors = require('../errors')
|
10 | const { check, verify } = require('../jwa')
|
11 | const JWK = require('../jwk')
|
12 |
|
13 | const { detect: resolveSerialization } = require('./serializers')
|
14 |
|
15 | validateCrit = validateCrit.bind(undefined, errors.JWSInvalid)
|
16 | const SINGLE_RECIPIENT = new Set(['compact', 'flattened', 'preparsed'])
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | const 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
|
39 | let header
|
40 | let payload
|
41 | let signature
|
42 | let alg
|
43 |
|
44 |
|
45 |
|
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':
|
59 | ([prot, payload, signature] = jws.split('.'))
|
60 | break
|
61 | case 'flattened':
|
62 | ({ protected: prot, payload, signature, header } = jws)
|
63 | break
|
64 | case 'preparsed': {
|
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 |
|
109 |
|
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 |
|
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 |
|
201 | module.exports = {
|
202 | bare: jwsVerify,
|
203 | verify: jwsVerify.bind(undefined, false, undefined)
|
204 | }
|