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