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