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}