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