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