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