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