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