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