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 2024-04-20 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(Objects.requireNonNull(payload, "The payload must not be null")); 283 284 if (signatures.isEmpty()) { 285 throw new IllegalArgumentException("At least one signature required"); 286 } 287 288 this.signatures.addAll(signatures); 289 } 290 291 292 /** 293 * Returns the individual signatures. 294 * 295 * @return The individual signatures, as an unmodified list, empty list 296 * if none have been added. 297 */ 298 public List<Signature> getSignatures() { 299 300 return Collections.unmodifiableList(signatures); 301 } 302 303 304 /** 305 * Signs this JWS secured object with the specified JWS signer and 306 * adds the resulting signature to it. To add multiple 307 * {@link #getSignatures() signatures} call this method successively. 308 * 309 * @param jwsHeader The JWS protected header. The algorithm specified 310 * by the header must be supported by the JWS signer. 311 * Must not be {@code null}. 312 * @param signer The JWS signer. Must not be {@code null}. 313 * 314 * @throws JOSEException If the JWS object couldn't be signed. 315 */ 316 public synchronized void sign(final JWSHeader jwsHeader, 317 final JWSSigner signer) 318 throws JOSEException { 319 320 sign(jwsHeader, null, signer); 321 } 322 323 324 /** 325 * Signs this JWS secured object with the specified JWS signer and 326 * adds the resulting signature to it. To add multiple 327 * {@link #getSignatures() signatures} call this method successively. 328 * 329 * @param jwsHeader The JWS protected header. The 330 * algorithm specified by the header must 331 * be supported by the JWS signer. Must 332 * not be {@code null}. 333 * @param unprotectedHeader The unprotected header to include, 334 * {@code null} if none. 335 * @param signer The JWS signer. Must not be 336 * {@code null}. 337 * 338 * @throws JOSEException If the JWS object couldn't be signed. 339 */ 340 public synchronized void sign(final JWSHeader jwsHeader, 341 final UnprotectedHeader unprotectedHeader, 342 final JWSSigner signer) 343 throws JOSEException { 344 345 try { 346 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 347 } catch (IllegalHeaderException e) { 348 throw new IllegalArgumentException(e.getMessage(), e); 349 } 350 351 JWSObject jwsObject = new JWSObject(jwsHeader, getPayload()); 352 jwsObject.sign(signer); 353 354 signatures.add(new Signature(getPayload(), jwsHeader, unprotectedHeader, jwsObject.getSignature())); 355 } 356 357 358 /** 359 * Returns the current signatures state. 360 * 361 * @return The state. 362 */ 363 public State getState() { 364 365 if (getSignatures().isEmpty()) { 366 return State.UNSIGNED; 367 } 368 369 for (Signature sig: getSignatures()) { 370 if (! sig.isVerified()) { 371 return State.SIGNED; 372 } 373 } 374 375 return State.VERIFIED; 376 } 377 378 379 @Override 380 public Map<String, Object> toGeneralJSONObject() { 381 382 if (signatures.size() < 1) { 383 throw new IllegalStateException("The general JWS JSON serialization requires at least one signature"); 384 } 385 386 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 387 jsonObject.put("payload", getPayload().toBase64URL().toString()); 388 389 List<Object> signaturesJSONArray = JSONArrayUtils.newJSONArray(); 390 391 for (Signature signature: getSignatures()) { 392 Map<String, Object> signatureJSONObject = signature.toJSONObject(); 393 signaturesJSONArray.add(signatureJSONObject); 394 } 395 396 jsonObject.put("signatures", signaturesJSONArray); 397 398 return jsonObject; 399 } 400 401 402 @Override 403 public Map<String, Object> toFlattenedJSONObject() { 404 405 if (signatures.size() != 1) { 406 throw new IllegalStateException("The flattened JWS JSON serialization requires exactly one signature"); 407 } 408 409 Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject(); 410 jsonObject.put("payload", getPayload().toBase64URL().toString()); 411 jsonObject.putAll(getSignatures().get(0).toJSONObject()); 412 return jsonObject; 413 } 414 415 416 @Override 417 public String serializeGeneral() { 418 return JSONObjectUtils.toJSONString(toGeneralJSONObject()); 419 } 420 421 422 @Override 423 public String serializeFlattened() { 424 return JSONObjectUtils.toJSONString(toFlattenedJSONObject()); 425 } 426 427 428 private static JWSHeader parseJWSHeader(final Map<String, Object> jsonObject) 429 throws ParseException { 430 431 Base64URL protectedHeader = JSONObjectUtils.getBase64URL(jsonObject, "protected"); 432 433 if (protectedHeader == null) { 434 throw new ParseException("Missing protected header (required by this library)", 0); 435 } 436 437 try { 438 return JWSHeader.parse(protectedHeader); 439 } catch (ParseException e) { 440 if ("Not a JWS header".equals(e.getMessage())) { 441 // alg required by this library (not the spec) 442 throw new ParseException("Missing JWS \"alg\" parameter in protected header (required by this library)", 0); 443 } 444 throw e; 445 } 446 } 447 448 449 /** 450 * Parses a JWS secured object from the specified JSON object 451 * representation. 452 * 453 * @param jsonObject The JSON object to parse. Must not be 454 * {@code null}. 455 * 456 * @return The JWS secured object. 457 * 458 * @throws ParseException If the JSON object couldn't be parsed to a 459 * JWS secured object. 460 */ 461 public static JWSObjectJSON parse(final Map<String, Object> jsonObject) 462 throws ParseException { 463 464 // Payload always present 465 Base64URL payloadB64URL = JSONObjectUtils.getBase64URL(jsonObject, "payload"); 466 467 if (payloadB64URL == null) { 468 throw new ParseException("Missing payload", 0); 469 } 470 471 Payload payload = new Payload(payloadB64URL); 472 473 // Signature present at top-level in flattened JSON 474 Base64URL topLevelSignatureB64 = JSONObjectUtils.getBase64URL(jsonObject, "signature"); 475 476 boolean flattened = topLevelSignatureB64 != null; 477 478 List<Signature> signatureList = new LinkedList<>(); 479 480 if (flattened) { 481 482 JWSHeader jwsHeader = parseJWSHeader(jsonObject); 483 484 UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header")); 485 486 // https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.2 487 // "The "signatures" member MUST NOT be present when using this syntax." 488 if (jsonObject.get("signatures") != null) { 489 throw new ParseException("The \"signatures\" member must not be present in flattened JWS JSON serialization", 0); 490 } 491 492 try { 493 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 494 } catch (IllegalHeaderException e) { 495 throw new ParseException(e.getMessage(), 0); 496 } 497 498 signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, topLevelSignatureB64)); 499 500 } else { 501 Map<String, Object>[] signatures = JSONObjectUtils.getJSONObjectArray(jsonObject, "signatures"); 502 if (signatures == null || signatures.length == 0) { 503 throw new ParseException("The \"signatures\" member must be present in general JSON Serialization", 0); 504 } 505 506 for (Map<String, Object> signatureJSONObject: signatures) { 507 508 JWSHeader jwsHeader = parseJWSHeader(signatureJSONObject); 509 510 UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(signatureJSONObject, "header")); 511 512 try { 513 HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader); 514 } catch (IllegalHeaderException e) { 515 throw new ParseException(e.getMessage(), 0); 516 } 517 518 Base64URL signatureB64 = JSONObjectUtils.getBase64URL(signatureJSONObject, "signature"); 519 520 if (signatureB64 == null) { 521 throw new ParseException("Missing \"signature\" member", 0); 522 } 523 524 signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, signatureB64)); 525 } 526 } 527 528 return new JWSObjectJSON(payload, signatureList); 529 } 530 531 532 /** 533 * Parses a JWS secured object from the specified JSON object string. 534 * 535 * @param json The JSON object string to parse. Must not be 536 * {@code null}. 537 * 538 * @return The JWS secured object. 539 * 540 * @throws ParseException If the string couldn't be parsed to a JWS 541 * secured object. 542 */ 543 public static JWSObjectJSON parse(final String json) 544 throws ParseException { 545 546 return parse(JSONObjectUtils.parse(json)); 547 } 548}