001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2023, Connect2id Ltd and contributors. 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; 019 020 021import com.nimbusds.jose.util.Base64URL; 022import com.nimbusds.jose.util.JSONArrayUtils; 023import com.nimbusds.jose.util.JSONObjectUtils; 024import net.jcip.annotations.Immutable; 025import net.jcip.annotations.ThreadSafe; 026 027import java.text.ParseException; 028import java.util.*; 029import java.util.concurrent.atomic.AtomicBoolean; 030 031 032/** 033 * JSON Web Signature (JWS) secured object with 034 * <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.2">JSON 035 * serialisation</a>. 036 * 037 * <p>This class is thread-safe. 038 * 039 * @author Alexander Martynov 040 * @author Vladimir Dzhuvinov 041 * @version 2021-10-09 042 */ 043@ThreadSafe 044public class JWSObjectJSON extends JOSEObjectJSON { 045 046 047 private static final long serialVersionUID = 1L; 048 049 050 /** 051 * Individual signature in a JWS secured object serialisable to JSON. 052 */ 053 @Immutable 054 public static final class Signature { 055 056 057 /** 058 * The payload. 059 */ 060 private final Payload payload; 061 062 063 /** 064 * The JWS protected header, {@code null} if none. 065 */ 066 private final JWSHeader header; 067 068 069 /** 070 * The unprotected header, {@code null} if none. 071 */ 072 private final UnprotectedHeader unprotectedHeader; 073 074 075 /** 076 * The signature. 077 */ 078 private final Base64URL signature; 079 080 081 /** 082 * The signature verified state. 083 */ 084 private final AtomicBoolean verified = new AtomicBoolean(false); 085 086 087 /** 088 * Creates a new parsed signature. 089 * 090 * @param payload The payload. Must not be 091 * {@code null}. 092 * @param header The JWS protected header, 093 * {@code null} if none. 094 * @param unprotectedHeader The unprotected header, 095 * {@code null} if none. 096 * @param signature The signature. Must not be 097 * {@code null}. 098 */ 099 private Signature(final Payload payload, 100 final JWSHeader header, 101 final UnprotectedHeader unprotectedHeader, 102 final Base64URL signature) { 103 104 Objects.requireNonNull(payload); 105 this.payload = payload; 106 107 this.header = header; 108 this.unprotectedHeader = unprotectedHeader; 109 110 Objects.requireNonNull(signature); 111 this.signature = signature; 112 } 113 114 115 /** 116 * Returns the JWS protected header. 117 * 118 * @return The JWS protected header, {@code null} if none. 119 */ 120 public JWSHeader getHeader() { 121 return header; 122 } 123 124 125 /** 126 * Returns the unprotected header. 127 * 128 * @return The unprotected header, {@code null} if none. 129 */ 130 public UnprotectedHeader getUnprotectedHeader() { 131 return unprotectedHeader; 132 } 133 134 135 /** 136 * Returns the signature. 137 * 138 * @return The signature. 139 */ 140 public Base64URL getSignature() { 141 return signature; 142 } 143 144 145 /** 146 * Returns a JSON object representation for use in the general 147 * and flattened serialisations. 148 * 149 * @return The JSON object. 150 */ 151 private Map<String, Object> toJSONObject() { 152 153 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 154 155 if (header != null) { 156 jsonObject.put("protected", header.toBase64URL().toString()); 157 } 158 159 if (unprotectedHeader != null && ! unprotectedHeader.getIncludedParams().isEmpty()) { 160 jsonObject.put("header", unprotectedHeader.toJSONObject()); 161 } 162 163 jsonObject.put("signature", signature.toString()); 164 165 return jsonObject; 166 } 167 168 169 /** 170 * Returns the compact JWS object representation of this 171 * individual signature. 172 * 173 * @return The JWS object serialisable to compact encoding. 174 */ 175 public JWSObject toJWSObject() { 176 177 try { 178 return new JWSObject(header.toBase64URL(), payload.toBase64URL(), signature); 179 } catch (ParseException e) { 180 throw new IllegalStateException(); 181 } 182 } 183 184 185 /** 186 * Returns {@code true} if the signature was successfully 187 * verified with a previous call to {@link #verify}. 188 * 189 * @return {@code true} if the signature was successfully 190 * verified, {@code false} if the signature is invalid 191 * or {@link #verify} was never called. 192 */ 193 public boolean isVerified() { 194 return verified.get(); 195 } 196 197 198 /** 199 * Checks the signature with the specified verifier. 200 * 201 * @param verifier The JWS verifier. Must not be {@code null}. 202 * 203 * @return {@code true} if the signature was successfully 204 * verified, else {@code false}. 205 * 206 * @throws JOSEException If the signature verification failed. 207 */ 208 public synchronized boolean verify(final JWSVerifier verifier) 209 throws JOSEException { 210 211 try { 212 verified.set(toJWSObject().verify(verifier)); 213 } catch (JOSEException e) { 214 throw e; 215 } catch (Exception e) { 216 // Prevent throwing unchecked exceptions at this point, 217 // see issue #20 218 throw new JOSEException(e.getMessage(), e); 219 } 220 221 return verified.get(); 222 } 223 } 224 225 226 /** 227 * Enumeration of the states of a JSON Web Signature (JWS) secured 228 * object serialisable to JSON. 229 */ 230 public enum State { 231 232 233 /** 234 * The object is not signed yet. 235 */ 236 UNSIGNED, 237 238 239 /** 240 * The object has one or more signatures; they are not (all) 241 * verified. 242 */ 243 SIGNED, 244 245 246 /** 247 * All signatures are verified. 248 */ 249 VERIFIED 250 } 251 252 253 /** 254 * The applied signatures. 255 */ 256 private final List<Signature> signatures = new LinkedList<>(); 257 258 259 /** 260 * Creates a new to-be-signed JSON Web Signature (JWS) secured object 261 * with the specified payload. 262 * 263 * @param payload The payload. Must not be {@code null}. 264 */ 265 public JWSObjectJSON(final Payload payload) { 266 267 super(payload); 268 Objects.requireNonNull(payload, "The payload must not be null"); 269 } 270 271 272 /** 273 * Creates a new JSON Web Signature (JWS) secured object with one or 274 * more signatures. 275 * 276 * @param payload The payload. Must not be {@code null}. 277 * @param signatures The signatures. Must be at least one. 278 */ 279 private JWSObjectJSON(final Payload payload, 280 final List<Signature> signatures) { 281 282 super(payload); 283 284 Objects.requireNonNull(payload, "The payload must not be null"); 285 286 if (signatures.isEmpty()) { 287 throw new IllegalArgumentException("At least one signature required"); 288 } 289 290 this.signatures.addAll(signatures); 291 } 292 293 294 /** 295 * Returns the individual signatures. 296 * 297 * @return The individual signatures, as an unmodified list, empty list 298 * if none have been added. 299 */ 300 public List<Signature> getSignatures() { 301 302 return Collections.unmodifiableList(signatures); 303 } 304 305 306 /** 307 * Signs this JWS secured object with the specified JWS signer and 308 * adds the resulting signature to it. To add multiple 309 * {@link #getSignatures() signatures} call this method successively. 310 * 311 * @param jwsHeader The JWS protected header. The algorithm specified 312 * by the header must be supported by the JWS signer. 313 * Must not be {@code null}. 314 * @param signer The JWS signer. Must not be {@code null}. 315 * 316 * @throws JOSEException If the JWS object couldn't be signed. 317 */ 318 public synchronized void sign(final JWSHeader jwsHeader, 319 final JWSSigner signer) 320 throws JOSEException { 321 322 sign(jwsHeader, null, signer); 323 } 324 325 326 /** 327 * Signs this JWS secured object with the specified JWS signer and 328 * adds the resulting signature to it. To add multiple 329 * {@link #getSignatures() signatures} call this method successively. 330 * 331 * @param jwsHeader The JWS protected header. The 332 * algorithm specified by the header must 333 * be supported by the JWS signer. Must 334 * not be {@code null}. 335 * @param unprotectedHeader The unprotected header to include, 336 * {@code null} if none. 337 * @param signer The JWS signer. Must not be 338 * {@code null}. 339 * 340 * @throws JOSEException If the JWS object couldn't be signed. 341 */ 342 public synchronized void sign(final JWSHeader jwsHeader, 343 final UnprotectedHeader unprotectedHeader, 344 final JWSSigner signer) 345 throws JOSEException { 346 347 try { 348 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 349 } catch (IllegalHeaderException e) { 350 throw new IllegalArgumentException(e.getMessage(), e); 351 } 352 353 JWSObject jwsObject = new JWSObject(jwsHeader, getPayload()); 354 jwsObject.sign(signer); 355 356 signatures.add(new Signature(getPayload(), jwsHeader, unprotectedHeader, jwsObject.getSignature())); 357 } 358 359 360 /** 361 * Returns the current signatures state. 362 * 363 * @return The state. 364 */ 365 public State getState() { 366 367 if (getSignatures().isEmpty()) { 368 return State.UNSIGNED; 369 } 370 371 for (Signature sig: getSignatures()) { 372 if (! sig.isVerified()) { 373 return State.SIGNED; 374 } 375 } 376 377 return State.VERIFIED; 378 } 379 380 381 @Override 382 public Map<String, Object> toGeneralJSONObject() { 383 384 if (signatures.size() < 1) { 385 throw new IllegalStateException("The general JWS JSON serialization requires at least one signature"); 386 } 387 388 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 389 jsonObject.put("payload", getPayload().toBase64URL().toString()); 390 391 List<Object> signaturesJSONArray = JSONArrayUtils.newJSONArray(); 392 393 for (Signature signature: getSignatures()) { 394 Map<String, Object> signatureJSONObject = signature.toJSONObject(); 395 signaturesJSONArray.add(signatureJSONObject); 396 } 397 398 jsonObject.put("signatures", signaturesJSONArray); 399 400 return jsonObject; 401 } 402 403 404 @Override 405 public Map<String, Object> toFlattenedJSONObject() { 406 407 if (signatures.size() != 1) { 408 throw new IllegalStateException("The flattened JWS JSON serialization requires exactly one signature"); 409 } 410 411 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 412 jsonObject.put("payload", getPayload().toBase64URL().toString()); 413 jsonObject.putAll(getSignatures().get(0).toJSONObject()); 414 return jsonObject; 415 } 416 417 418 @Override 419 public String serializeGeneral() { 420 return JSONObjectUtils.toJSONString(toGeneralJSONObject()); 421 } 422 423 424 @Override 425 public String serializeFlattened() { 426 return JSONObjectUtils.toJSONString(toFlattenedJSONObject()); 427 } 428 429 430 private static JWSHeader parseJWSHeader(final Map<String, Object> jsonObject) 431 throws ParseException { 432 433 Base64URL protectedHeader = JSONObjectUtils.getBase64URL(jsonObject, "protected"); 434 435 if (protectedHeader == null) { 436 throw new ParseException("Missing protected header (required by this library)", 0); 437 } 438 439 try { 440 return JWSHeader.parse(protectedHeader); 441 } catch (ParseException e) { 442 if ("Not a JWS header".equals(e.getMessage())) { 443 // alg required by this library (not the spec) 444 throw new ParseException("Missing JWS \"alg\" parameter in protected header (required by this library)", 0); 445 } 446 throw e; 447 } 448 } 449 450 451 /** 452 * Parses a JWS secured object from the specified JSON object 453 * representation. 454 * 455 * @param jsonObject The JSON object to parse. Must not be 456 * {@code null}. 457 * 458 * @return The JWS secured object. 459 * 460 * @throws ParseException If the JSON object couldn't be parsed to a 461 * JWS secured object. 462 */ 463 public static JWSObjectJSON parse(final Map<String, Object> jsonObject) 464 throws ParseException { 465 466 // Payload always present 467 Base64URL payloadB64URL = JSONObjectUtils.getBase64URL(jsonObject, "payload"); 468 469 if (payloadB64URL == null) { 470 throw new ParseException("Missing payload", 0); 471 } 472 473 Payload payload = new Payload(payloadB64URL); 474 475 // Signature present at top-level in flattened JSON 476 Base64URL topLevelSignatureB64 = JSONObjectUtils.getBase64URL(jsonObject, "signature"); 477 478 boolean flattened = topLevelSignatureB64 != null; 479 480 List<Signature> signatureList = new LinkedList<>(); 481 482 if (flattened) { 483 484 JWSHeader jwsHeader = parseJWSHeader(jsonObject); 485 486 UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header")); 487 488 // https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.2 489 // "The "signatures" member MUST NOT be present when using this syntax." 490 if (jsonObject.get("signatures") != null) { 491 throw new ParseException("The \"signatures\" member must not be present in flattened JWS JSON serialization", 0); 492 } 493 494 try { 495 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 496 } catch (IllegalHeaderException e) { 497 throw new ParseException(e.getMessage(), 0); 498 } 499 500 signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, topLevelSignatureB64)); 501 502 } else { 503 Map<String, Object>[] signatures = JSONObjectUtils.getJSONObjectArray(jsonObject, "signatures"); 504 if (signatures == null || signatures.length == 0) { 505 throw new ParseException("The \"signatures\" member must be present in general JSON Serialization", 0); 506 } 507 508 for (Map<String, Object> signatureJSONObject: signatures) { 509 510 JWSHeader jwsHeader = parseJWSHeader(signatureJSONObject); 511 512 UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(signatureJSONObject, "header")); 513 514 try { 515 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 516 } catch (IllegalHeaderException e) { 517 throw new ParseException(e.getMessage(), 0); 518 } 519 520 Base64URL signatureB64 = JSONObjectUtils.getBase64URL(signatureJSONObject, "signature"); 521 522 if (signatureB64 == null) { 523 throw new ParseException("Missing \"signature\" member", 0); 524 } 525 526 signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, signatureB64)); 527 } 528 } 529 530 return new JWSObjectJSON(payload, signatureList); 531 } 532 533 534 /** 535 * Parses a JWS secured object from the specified JSON object string. 536 * 537 * @param json The JSON object string to parse. Must not be 538 * {@code null}. 539 * 540 * @return The JWS secured object. 541 * 542 * @throws ParseException If the string couldn't be parsed to a JWS 543 * secured object. 544 */ 545 public static JWSObjectJSON parse(final String json) 546 throws ParseException { 547 548 return parse(JSONObjectUtils.parse(json)); 549 } 550}