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