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