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}