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