001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, 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.net.URL;
024import java.nio.charset.Charset;
025import java.security.KeyStore;
026import java.security.KeyStoreException;
027import java.security.cert.Certificate;
028import java.security.interfaces.ECPublicKey;
029import java.security.interfaces.RSAPublicKey;
030import java.text.ParseException;
031import java.util.*;
032
033import com.nimbusds.jose.JOSEException;
034import com.nimbusds.jose.util.*;
035import net.jcip.annotations.Immutable;
036import net.minidev.json.JSONArray;
037import net.minidev.json.JSONObject;
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 JSON Web Key (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 * @version 2018-03-09
071 */
072@Immutable
073public class JWKSet {
074
075
076        /**
077         * The MIME type of JWK set objects: 
078         * {@code application/jwk-set+json; charset=UTF-8}
079         */
080        public static final String MIME_TYPE = "application/jwk-set+json; charset=UTF-8";
081
082
083        /**
084         * The JWK list.
085         */
086        private final List<JWK> keys;
087
088
089        /**
090         * Additional custom members.
091         */
092        private final Map<String,Object> customMembers;
093
094
095        /**
096         * Creates a new empty JSON Web Key (JWK) set.
097         */
098        public JWKSet() {
099
100                this(Collections.<JWK>emptyList());
101        }
102
103
104        /**
105         * Creates a new JSON Web Key (JWK) set with a single key.
106         *
107         * @param key The JWK. Must not be {@code null}.
108         */
109        public JWKSet(final JWK key) {
110                
111                this(Collections.singletonList(key));
112                
113                if (key == null) {
114                        throw new IllegalArgumentException("The JWK must not be null");
115                }
116        }
117
118
119        /**
120         * Creates a new JSON Web Key (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 JSON Web Key (JWK) set with the specified keys and
132         * additional custom 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                if (keys == null) {
141                        throw new IllegalArgumentException("The JWK list must not be null");
142                }
143
144                this.keys = Collections.unmodifiableList(keys);
145
146                this.customMembers = Collections.unmodifiableMap(customMembers);
147        }
148
149
150        /**
151         * Gets the keys (ordered) of this JSON Web Key (JWK) set.
152         *
153         * @return The keys, empty list if none.
154         */
155        public List<JWK> getKeys() {
156
157                return keys;
158        }
159
160        
161        /**
162         * Gets the key from this JSON Web Key (JWK) set as identified by its 
163         * Key ID (kid) member.
164         * 
165         * <p>If more than one key exists in the JWK Set with the same 
166         * identifier, this function returns only the first one in the set.
167         *
168         * @param kid They key identifier.
169         *
170         * @return The key identified by {@code kid} or {@code null} if no key 
171         *         exists.
172         */
173        public JWK getKeyByKeyId(String kid) {
174                
175                for (JWK key : getKeys()) {
176                
177                        if (key.getKeyID() != null && key.getKeyID().equals(kid)) {
178                                return key;
179                        }
180                }
181                
182                // no key found
183                return null;
184        }
185
186
187        /**
188         * Gets the additional custom members of this JSON Web Key (JWK) set.
189         *
190         * @return The additional custom members, empty map if none.
191         */
192        public Map<String,Object> getAdditionalMembers() {
193
194                return customMembers;
195        }
196
197
198        /**
199         * Returns a copy of this JSON Web Key (JWK) set with all private keys
200         * and parameters removed.
201         *
202         * @return A copy of this JWK set with all private keys and parameters
203         *         removed.
204         */
205        public JWKSet toPublicJWKSet() {
206
207                List<JWK> publicKeyList = new LinkedList<>();
208
209                for (JWK key: keys) {
210
211                        JWK publicKey = key.toPublicJWK();
212
213                        if (publicKey != null) {
214                                publicKeyList.add(publicKey);
215                        }
216                }
217
218                return new JWKSet(publicKeyList, customMembers);
219        }
220
221
222        /**
223         * Returns the JSON object representation of this JSON Web Key (JWK) 
224         * set. Private keys and parameters will be omitted from the output.
225         * Use the alternative {@link #toJSONObject(boolean)} method if you
226         * wish to include them.
227         *
228         * @return The JSON object representation.
229         */
230        public JSONObject toJSONObject() {
231
232                return toJSONObject(true);
233        }
234
235
236        /**
237         * Returns the JSON object representation of this JSON Web Key (JWK) 
238         * set.
239         *
240         * @param publicKeysOnly Controls the inclusion of private keys and
241         *                       parameters into the output JWK members. If
242         *                       {@code true} private keys and parameters will
243         *                       be omitted. If {@code false} all available key
244         *                       parameters will be included.
245         *
246         * @return The JSON object representation.
247         */
248        public JSONObject toJSONObject(final boolean publicKeysOnly) {
249
250                JSONObject o = new JSONObject(customMembers);
251
252                JSONArray a = new JSONArray();
253
254                for (JWK key: keys) {
255
256                        if (publicKeysOnly) {
257
258                                // Try to get public key, then serialise
259                                JWK publicKey = key.toPublicJWK();
260
261                                if (publicKey != null) {
262                                        a.add(publicKey.toJSONObject());
263                                }
264                        } else {
265
266                                a.add(key.toJSONObject());
267                        }
268                }
269
270                o.put("keys", a);
271
272                return o;
273        }
274
275
276        /**
277         * Returns the JSON object string representation of this JSON Web Key
278         * (JWK) set.
279         *
280         * @return The JSON object string representation.
281         */
282        @Override
283        public String toString() {
284
285                return toJSONObject().toString();
286        }
287
288
289        /**
290         * Parses the specified string representing a JSON Web Key (JWK) set.
291         *
292         * @param s The string to parse. Must not be {@code null}.
293         *
294         * @return The JWK set.
295         *
296         * @throws ParseException If the string couldn't be parsed to a valid
297         *                        JSON Web Key (JWK) set.
298         */
299        public static JWKSet parse(final String s)
300                throws ParseException {
301
302                return parse(JSONObjectUtils.parse(s));
303        }
304
305
306        /**
307         * Parses the specified JSON object representing a JSON Web Key (JWK) 
308         * set.
309         *
310         * @param json The JSON object to parse. Must not be {@code null}.
311         *
312         * @return The JWK set.
313         *
314         * @throws ParseException If the string couldn't be parsed to a valid
315         *                        JSON Web Key (JWK) set.
316         */
317        public static JWKSet parse(final JSONObject json)
318                throws ParseException {
319
320                JSONArray keyArray = JSONObjectUtils.getJSONArray(json, "keys");
321
322                List<JWK> keys = new LinkedList<>();
323
324                for (int i=0; i < keyArray.size(); i++) {
325
326                        if (! (keyArray.get(i) instanceof JSONObject)) {
327                                throw new ParseException("The \"keys\" JSON array must contain JSON objects only", 0);
328                        }
329
330                        JSONObject keyJSON = (JSONObject)keyArray.get(i);
331
332                        try {
333                                keys.add(JWK.parse(keyJSON));
334
335                        } catch (ParseException e) {
336
337                                throw new ParseException("Invalid JWK at position " + i + ": " + e.getMessage(), 0);
338                        }
339                }
340
341                // Parse additional custom members
342                Map<String, Object> additionalMembers = new HashMap<>();
343                for (Map.Entry<String,Object> entry: json.entrySet()) {
344                        
345                        if (entry.getKey() == null || entry.getKey().equals("keys")) {
346                                continue;
347                        }
348                        
349                        additionalMembers.put(entry.getKey(), entry.getValue());
350                }
351                
352                return new JWKSet(keys, additionalMembers);
353        }
354
355
356        /**
357         * Loads a JSON Web Key (JWK) set from the specified file.
358         *
359         * @param file The JWK set file. Must not be {@code null}.
360         *
361         * @return The JWK set.
362         *
363         * @throws IOException    If the file couldn't be read.
364         * @throws ParseException If the file couldn't be parsed to a valid
365         *                        JSON Web Key (JWK) set.
366         */
367        public static JWKSet load(final File file)
368                throws IOException, ParseException {
369
370                return parse(IOUtils.readFileToString(file, Charset.forName("UTF-8")));
371        }
372
373
374        /**
375         * Loads a JSON Web Key (JWK) set from the specified URL.
376         *
377         * @param url            The JWK set URL. Must not be {@code null}.
378         * @param connectTimeout The URL connection timeout, in milliseconds.
379         *                       If zero no (infinite) timeout.
380         * @param readTimeout    The URL read timeout, in milliseconds. If zero
381         *                       no (infinite) timeout.
382         * @param sizeLimit      The read size limit, in bytes. If zero no
383         *                       limit.
384         *
385         * @return The JWK set.
386         *
387         * @throws IOException    If the file couldn't be read.
388         * @throws ParseException If the file couldn't be parsed to a valid
389         *                        JSON Web Key (JWK) set.
390         */
391        public static JWKSet load(final URL url,
392                                  final int connectTimeout,
393                                  final int readTimeout,
394                                  final int sizeLimit)
395                throws IOException, ParseException {
396
397                RestrictedResourceRetriever resourceRetriever = new DefaultResourceRetriever(
398                        connectTimeout,
399                        readTimeout,
400                        sizeLimit);
401                Resource resource = resourceRetriever.retrieveResource(url);
402                return parse(resource.getContent());
403        }
404
405
406        /**
407         * Loads a JSON Web Key (JWK) set from the specified URL.
408         *
409         * @param url The JWK set URL. Must not be {@code null}.
410         *
411         * @return The JWK set.
412         *
413         * @throws IOException    If the file couldn't be read.
414         * @throws ParseException If the file couldn't be parsed to a valid
415         *                        JSON Web Key (JWK) set.
416         */
417        public static JWKSet load(final URL url)
418                throws IOException, ParseException {
419
420                return load(url, 0, 0, 0);
421        }
422        
423        
424        /**
425         * Loads a JSON Web Key (JWK) set from the specified JCA key store. Key
426         * conversion exceptions are silently swallowed. PKCS#11 stores are
427         * also supported. Requires BouncyCastle.
428         *
429         * <p><strong>Important:</strong> The X.509 certificates are not
430         * validated!
431         *
432         * @param keyStore The key store. Must not be {@code null}.
433         * @param pwLookup The password lookup for password-protected keys,
434         *                 {@code null} if not specified.
435         *
436         * @return The JWK set, empty if no keys were loaded.
437         *
438         * @throws KeyStoreException On a key store exception.
439         */
440        public static JWKSet load(final KeyStore keyStore, final PasswordLookup pwLookup)
441                throws KeyStoreException {
442                
443                List<JWK> jwks = new LinkedList<>();
444                
445                // Load RSA and EC keys
446                for (Enumeration<String> keyAliases = keyStore.aliases(); keyAliases.hasMoreElements(); ) {
447                        
448                        final String keyAlias = keyAliases.nextElement();
449                        final char[] keyPassword = pwLookup == null ? "".toCharArray() : pwLookup.lookupPassword(keyAlias);
450                        
451                        Certificate cert = keyStore.getCertificate(keyAlias);
452                        if (cert == null) {
453                                continue; // skip
454                        }
455                        
456                        if (cert.getPublicKey() instanceof RSAPublicKey) {
457                                
458                                RSAKey rsaJWK;
459                                try {
460                                        rsaJWK = RSAKey.load(keyStore, keyAlias, keyPassword);
461                                } catch (JOSEException e) {
462                                        continue; // skip cert
463                                }
464                                
465                                if (rsaJWK == null) {
466                                        continue; // skip key
467                                }
468                                
469                                jwks.add(rsaJWK);
470                                
471                        } else if (cert.getPublicKey() instanceof ECPublicKey) {
472                                
473                                ECKey ecJWK;
474                                try {
475                                        ecJWK = ECKey.load(keyStore, keyAlias, keyPassword);
476                                } catch (JOSEException e) {
477                                        continue; // skip cert
478                                }
479                                
480                                if (ecJWK != null) {
481                                        jwks.add(ecJWK);
482                                }
483                                
484                        } else {
485                                continue;
486                        }
487                }
488                
489                
490                // Load symmetric keys
491                for (Enumeration<String> keyAliases = keyStore.aliases(); keyAliases.hasMoreElements(); ) {
492                        
493                        final String keyAlias = keyAliases.nextElement();
494                        final char[] keyPassword = pwLookup == null ? "".toCharArray() : pwLookup.lookupPassword(keyAlias);
495                        
496                        OctetSequenceKey octJWK;
497                        try {
498                                octJWK = OctetSequenceKey.load(keyStore, keyAlias, keyPassword);
499                        } catch (JOSEException e) {
500                                continue; // skip key
501                        }
502                        
503                        if (octJWK != null) {
504                                jwks.add(octJWK);
505                        }
506                }
507                
508                return new JWKSet(jwks);
509        }
510}