001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2018, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.jose.jwk;
019
020
021import java.io.File;
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.Proxy;
025import java.net.URL;
026import java.nio.charset.Charset;
027import java.security.KeyStore;
028import java.security.KeyStoreException;
029import java.security.cert.Certificate;
030import java.security.interfaces.ECPublicKey;
031import java.security.interfaces.RSAPublicKey;
032import java.text.ParseException;
033import java.util.*;
034
035import com.nimbusds.jose.JOSEException;
036import com.nimbusds.jose.util.*;
037import net.jcip.annotations.Immutable;
038import net.minidev.json.JSONArray;
039import net.minidev.json.JSONObject;
040
041
042/**
043 * JSON Web Key (JWK) set. Represented by a JSON object that contains an array
044 * of {@link JWK JSON Web Keys} (JWKs) as the value of its "keys" member.
045 * Additional (custom) members of the JWK Set JSON object are also supported.
046 *
047 * <p>Example JSON Web Key (JWK) set:
048 *
049 * <pre>
050 * {
051 *   "keys" : [ { "kty" : "EC",
052 *                "crv" : "P-256",
053 *                "x"   : "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
054 *                "y"   : "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
055 *                "use" : "enc",
056 *                "kid" : "1" },
057 *
058 *              { "kty" : "RSA",
059 *                "n"   : "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
060 *                         4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
061 *                         tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
062 *                         QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
063 *                         SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
064 *                         w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
065 *                "e"   : "AQAB",
066 *                "alg" : "RS256",
067 *                "kid" : "2011-04-29" } ]
068 * }
069 * </pre>
070 *
071 * @author Vladimir Dzhuvinov
072 * @author Vedran Pavic
073 * @version 2019-08-23
074 */
075@Immutable
076public class JWKSet {
077
078
079        /**
080         * The MIME type of JWK set objects: 
081         * {@code application/jwk-set+json; charset=UTF-8}
082         */
083        public static final String MIME_TYPE = "application/jwk-set+json; charset=UTF-8";
084
085
086        /**
087         * The JWK list.
088         */
089        private final List<JWK> keys;
090
091
092        /**
093         * Additional custom members.
094         */
095        private final Map<String,Object> customMembers;
096
097
098        /**
099         * Creates a new empty JSON Web Key (JWK) set.
100         */
101        public JWKSet() {
102
103                this(Collections.<JWK>emptyList());
104        }
105
106
107        /**
108         * Creates a new JSON Web Key (JWK) set with a single key.
109         *
110         * @param key The JWK. Must not be {@code null}.
111         */
112        public JWKSet(final JWK key) {
113                
114                this(Collections.singletonList(key));
115                
116                if (key == null) {
117                        throw new IllegalArgumentException("The JWK must not be null");
118                }
119        }
120
121
122        /**
123         * Creates a new JSON Web Key (JWK) set with the specified keys.
124         *
125         * @param keys The JWK list. Must not be {@code null}.
126         */
127        public JWKSet(final List<JWK> keys) {
128
129                this(keys, Collections.<String, Object>emptyMap());
130        }
131
132
133        /**
134         * Creates a new JSON Web Key (JWK) set with the specified keys and
135         * additional custom members.
136         *
137         * @param keys          The JWK list. Must not be {@code null}.
138         * @param customMembers The additional custom members. Must not be
139         *                      {@code null}.
140         */
141        public JWKSet(final List<JWK> keys, final Map<String,Object> customMembers) {
142
143                if (keys == null) {
144                        throw new IllegalArgumentException("The JWK list must not be null");
145                }
146
147                this.keys = Collections.unmodifiableList(keys);
148
149                this.customMembers = Collections.unmodifiableMap(customMembers);
150        }
151
152
153        /**
154         * Gets the keys (ordered) of this JSON Web Key (JWK) set.
155         *
156         * @return The keys, empty list if none.
157         */
158        public List<JWK> getKeys() {
159
160                return keys;
161        }
162
163        
164        /**
165         * Gets the key from this JSON Web Key (JWK) set as identified by its 
166         * Key ID (kid) member.
167         * 
168         * <p>If more than one key exists in the JWK Set with the same 
169         * identifier, this function returns only the first one in the set.
170         *
171         * @param kid They key identifier.
172         *
173         * @return The key identified by {@code kid} or {@code null} if no key 
174         *         exists.
175         */
176        public JWK getKeyByKeyId(String kid) {
177                
178                for (JWK key : getKeys()) {
179                
180                        if (key.getKeyID() != null && key.getKeyID().equals(kid)) {
181                                return key;
182                        }
183                }
184                
185                // no key found
186                return null;
187        }
188
189
190        /**
191         * Gets the additional custom members of this JSON Web Key (JWK) set.
192         *
193         * @return The additional custom members, empty map if none.
194         */
195        public Map<String,Object> getAdditionalMembers() {
196
197                return customMembers;
198        }
199
200
201        /**
202         * Returns a copy of this JSON Web Key (JWK) set with all private keys
203         * and parameters removed.
204         *
205         * @return A copy of this JWK set with all private keys and parameters
206         *         removed.
207         */
208        public JWKSet toPublicJWKSet() {
209
210                List<JWK> publicKeyList = new LinkedList<>();
211
212                for (JWK key: keys) {
213
214                        JWK publicKey = key.toPublicJWK();
215
216                        if (publicKey != null) {
217                                publicKeyList.add(publicKey);
218                        }
219                }
220
221                return new JWKSet(publicKeyList, customMembers);
222        }
223
224
225        /**
226         * Returns the JSON object representation of this JSON Web Key (JWK) 
227         * set. Private keys and parameters will be omitted from the output.
228         * Use the alternative {@link #toJSONObject(boolean)} method if you
229         * wish to include them.
230         *
231         * @return The JSON object representation.
232         */
233        public JSONObject toJSONObject() {
234
235                return toJSONObject(true);
236        }
237
238
239        /**
240         * Returns the JSON object representation of this JSON Web Key (JWK) 
241         * set.
242         *
243         * @param publicKeysOnly Controls the inclusion of private keys and
244         *                       parameters into the output JWK members. If
245         *                       {@code true} private keys and parameters will
246         *                       be omitted. If {@code false} all available key
247         *                       parameters will be included.
248         *
249         * @return The JSON object representation.
250         */
251        public JSONObject toJSONObject(final boolean publicKeysOnly) {
252
253                JSONObject o = new JSONObject(customMembers);
254
255                JSONArray a = new JSONArray();
256
257                for (JWK key: keys) {
258
259                        if (publicKeysOnly) {
260
261                                // Try to get public key, then serialise
262                                JWK publicKey = key.toPublicJWK();
263
264                                if (publicKey != null) {
265                                        a.add(publicKey.toJSONObject());
266                                }
267                        } else {
268
269                                a.add(key.toJSONObject());
270                        }
271                }
272
273                o.put("keys", a);
274
275                return o;
276        }
277
278
279        /**
280         * Returns the JSON object string representation of this JSON Web Key
281         * (JWK) set.
282         *
283         * @return The JSON object string representation.
284         */
285        @Override
286        public String toString() {
287
288                return toJSONObject().toString();
289        }
290
291
292        /**
293         * Parses the specified string representing a JSON Web Key (JWK) set.
294         *
295         * @param s The string to parse. Must not be {@code null}.
296         *
297         * @return The JWK set.
298         *
299         * @throws ParseException If the string couldn't be parsed to a valid
300         *                        JSON Web Key (JWK) set.
301         */
302        public static JWKSet parse(final String s)
303                throws ParseException {
304
305                return parse(JSONObjectUtils.parse(s));
306        }
307
308
309        /**
310         * Parses the specified JSON object representing a JSON Web Key (JWK) 
311         * set.
312         *
313         * @param json The JSON object to parse. Must not be {@code null}.
314         *
315         * @return The JWK set.
316         *
317         * @throws ParseException If the string couldn't be parsed to a valid
318         *                        JSON Web Key (JWK) set.
319         */
320        public static JWKSet parse(final JSONObject json)
321                throws ParseException {
322
323                JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys");
324                
325                if (keyArray == null) {
326                        throw new ParseException("Missing required \"keys\" member", 0);
327                }
328
329                List<JWK> keys = new LinkedList<>();
330
331                for (int i=0; i < keyArray.size(); i++) {
332
333                        if (! (keyArray.get(i) instanceof JSONObject)) {
334                                throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0);
335                        }
336
337                        JSONObject keyJSON = (JSONObject)keyArray.get(i);
338
339                        try {
340                                keys.add(JWK.parse(keyJSON));
341
342                        } catch (ParseException e) {
343
344                                throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0);
345                        }
346                }
347
348                // Parse additional custom members
349                Map<String, Object> additionalMembers = new HashMap<>();
350                for (Map.Entry<String,Object> entry: json.entrySet()) {
351                        
352                        if (entry.getKey() == null || entry.getKey().equals("keys")) {
353                                continue;
354                        }
355                        
356                        additionalMembers.put(entry.getKey(), entry.getValue());
357                }
358                
359                return new JWKSet(keys, additionalMembers);
360        }
361
362
363        /**
364         * Loads a JSON Web Key (JWK) set from the specified input stream.
365         *
366         * @param inputStream The JWK set input stream. Must not be {@code null}.
367         *
368         * @return The JWK set.
369         *
370         * @throws IOException    If the input stream couldn't be read.
371         * @throws ParseException If the input stream couldn't be parsed to a valid
372         *                        JSON Web Key (JWK) set.
373         */
374        public static JWKSet load(final InputStream inputStream)
375                throws IOException, ParseException {
376
377                return parse(IOUtils.readInputStreamToString(inputStream, Charset.forName("UTF-8")));
378        }
379
380
381        /**
382         * Loads a JSON Web Key (JWK) set from the specified file.
383         *
384         * @param file The JWK set file. Must not be {@code null}.
385         *
386         * @return The JWK set.
387         *
388         * @throws IOException    If the file couldn't be read.
389         * @throws ParseException If the file couldn't be parsed to a valid
390         *                        JSON Web Key (JWK) set.
391         */
392        public static JWKSet load(final File file)
393                throws IOException, ParseException {
394
395                return parse(IOUtils.readFileToString(file, Charset.forName("UTF-8")));
396        }
397
398
399        /**
400         * Loads a JSON Web Key (JWK) set from the specified URL.
401         *
402         * @param url            The JWK set URL. Must not be {@code null}.
403         * @param connectTimeout The URL connection timeout, in milliseconds.
404         *                       If zero no (infinite) timeout.
405         * @param readTimeout    The URL read timeout, in milliseconds. If zero
406         *                       no (infinite) timeout.
407         * @param sizeLimit      The read size limit, in bytes. If zero no
408         *                       limit.
409         *
410         * @return The JWK set.
411         *
412         * @throws IOException    If the file couldn't be read.
413         * @throws ParseException If the file couldn't be parsed to a valid
414         *                        JSON Web Key (JWK) set.
415         */
416        public static JWKSet load(final URL url,
417                                  final int connectTimeout,
418                                  final int readTimeout,
419                                  final int sizeLimit)
420                throws IOException, ParseException {
421
422                return load(url, connectTimeout, readTimeout, sizeLimit, null);
423        }
424
425
426        /**
427         * Loads a JSON Web Key (JWK) set from the specified URL.
428         *
429         * @param url            The JWK set URL. Must not be {@code null}.
430         * @param connectTimeout The URL connection timeout, in milliseconds.
431         *                       If zero no (infinite) timeout.
432         * @param readTimeout    The URL read timeout, in milliseconds. If zero
433         *                       no (infinite) timeout.
434         * @param sizeLimit      The read size limit, in bytes. If zero no
435         *                       limit.
436         * @param proxy          The optional proxy to use when opening the
437         *                       connection to retrieve the resource. If
438         *                       {@code null}, no proxy is used.
439         *
440         * @return The JWK set.
441         *
442         * @throws IOException    If the file couldn't be read.
443         * @throws ParseException If the file couldn't be parsed to a valid
444         *                        JSON Web Key (JWK) set.
445         */
446        public static JWKSet load(final URL url,
447                                  final int connectTimeout,
448                                  final int readTimeout,
449                                  final int sizeLimit,
450                                  final Proxy proxy)
451                        throws IOException, ParseException {
452
453                DefaultResourceRetriever resourceRetriever = new DefaultResourceRetriever(
454                                connectTimeout,
455                                readTimeout,
456                                sizeLimit);
457                resourceRetriever.setProxy(proxy);
458                Resource resource = resourceRetriever.retrieveResource(url);
459                return parse(resource.getContent());
460        }
461
462
463        /**
464         * Loads a JSON Web Key (JWK) set from the specified URL.
465         *
466         * @param url The JWK set URL. Must not be {@code null}.
467         *
468         * @return The JWK set.
469         *
470         * @throws IOException    If the file couldn't be read.
471         * @throws ParseException If the file couldn't be parsed to a valid
472         *                        JSON Web Key (JWK) set.
473         */
474        public static JWKSet load(final URL url)
475                throws IOException, ParseException {
476
477                return load(url, 0, 0, 0);
478        }
479        
480        
481        /**
482         * Loads a JSON Web Key (JWK) set from the specified JCA key store. Key
483         * conversion exceptions are silently swallowed. PKCS#11 stores are
484         * also supported. Requires BouncyCastle.
485         *
486         * <p><strong>Important:</strong> The X.509 certificates are not
487         * validated!
488         *
489         * @param keyStore The key store. Must not be {@code null}.
490         * @param pwLookup The password lookup for password-protected keys,
491         *                 {@code null} if not specified.
492         *
493         * @return The JWK set, empty if no keys were loaded.
494         *
495         * @throws KeyStoreException On a key store exception.
496         */
497        public static JWKSet load(final KeyStore keyStore, final PasswordLookup pwLookup)
498                throws KeyStoreException {
499                
500                List<JWK> jwks = new LinkedList<>();
501                
502                // Load RSA and EC keys
503                for (Enumeration<String> keyAliases = keyStore.aliases(); keyAliases.hasMoreElements(); ) {
504                        
505                        final String keyAlias = keyAliases.nextElement();
506                        final char[] keyPassword = pwLookup == null ? "".toCharArray() : pwLookup.lookupPassword(keyAlias);
507                        
508                        Certificate cert = keyStore.getCertificate(keyAlias);
509                        if (cert == null) {
510                                continue; // skip
511                        }
512                        
513                        if (cert.getPublicKey() instanceof RSAPublicKey) {
514                                
515                                RSAKey rsaJWK;
516                                try {
517                                        rsaJWK = RSAKey.load(keyStore, keyAlias, keyPassword);
518                                } catch (JOSEException e) {
519                                        continue; // skip cert
520                                }
521                                
522                                if (rsaJWK == null) {
523                                        continue; // skip key
524                                }
525                                
526                                jwks.add(rsaJWK);
527                                
528                        } else if (cert.getPublicKey() instanceof ECPublicKey) {
529                                
530                                ECKey ecJWK;
531                                try {
532                                        ecJWK = ECKey.load(keyStore, keyAlias, keyPassword);
533                                } catch (JOSEException e) {
534                                        continue; // skip cert
535                                }
536                                
537                                if (ecJWK != null) {
538                                        jwks.add(ecJWK);
539                                }
540                                
541                        } else {
542                                continue;
543                        }
544                }
545                
546                
547                // Load symmetric keys
548                for (Enumeration<String> keyAliases = keyStore.aliases(); keyAliases.hasMoreElements(); ) {
549                        
550                        final String keyAlias = keyAliases.nextElement();
551                        final char[] keyPassword = pwLookup == null ? "".toCharArray() : pwLookup.lookupPassword(keyAlias);
552                        
553                        OctetSequenceKey octJWK;
554                        try {
555                                octJWK = OctetSequenceKey.load(keyStore, keyAlias, keyPassword);
556                        } catch (JOSEException e) {
557                                continue; // skip key
558                        }
559                        
560                        if (octJWK != null) {
561                                jwks.add(octJWK);
562                        }
563                }
564                
565                return new JWKSet(jwks);
566        }
567}