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