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; 019 020 021import java.text.ParseException; 022import java.util.concurrent.atomic.AtomicReference; 023 024import net.jcip.annotations.ThreadSafe; 025 026import com.nimbusds.jose.util.Base64URL; 027import com.nimbusds.jose.util.StandardCharset; 028 029 030/** 031 * JSON Web Signature (JWS) secured object serialisable to 032 * <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-7.1">compact 033 * encoding</a>. 034 * 035 * <p>This class is thread-safe. 036 * 037 * @author Vladimir Dzhuvinov 038 * @version 2022-09-27 039 */ 040@ThreadSafe 041public class JWSObject extends JOSEObject { 042 043 044 private static final long serialVersionUID = 1L; 045 046 047 /** 048 * Enumeration of the states of a JSON Web Signature (JWS) secured 049 * object. 050 */ 051 public enum State { 052 053 054 /** 055 * The object is not signed yet. 056 */ 057 UNSIGNED, 058 059 060 /** 061 * The JWS secured object is signed but its signature is not 062 * verified. 063 */ 064 SIGNED, 065 066 067 /** 068 * The JWS secured object is signed and its signature was 069 * successfully verified. 070 */ 071 VERIFIED 072 } 073 074 075 /** 076 * The header. 077 */ 078 private final JWSHeader header; 079 080 081 /** 082 * The signing input for this JWS object. 083 */ 084 private final String signingInputString; 085 086 087 /** 088 * The signature, {@code null} if not signed. 089 */ 090 private Base64URL signature; 091 092 093 /** 094 * The JWS object state. 095 */ 096 private final AtomicReference<State> state = new AtomicReference<>(); 097 098 099 /** 100 * Creates a new to-be-signed JSON Web Signature (JWS) object with the 101 * specified header and payload. The initial state will be 102 * {@link State#UNSIGNED unsigned}. 103 * 104 * @param header The JWS header. Must not be {@code null}. 105 * @param payload The payload. Must not be {@code null}. 106 */ 107 public JWSObject(final JWSHeader header, final Payload payload) { 108 109 if (header == null) { 110 throw new IllegalArgumentException("The JWS header must not be null"); 111 } 112 this.header = header; 113 114 if (payload == null) { 115 throw new IllegalArgumentException("The payload must not be null"); 116 } 117 setPayload(payload); 118 119 signingInputString = composeSigningInput(); 120 signature = null; 121 state.set(State.UNSIGNED); 122 } 123 124 125 /** 126 * Creates a new signed JSON Web Signature (JWS) object with the 127 * specified serialised parts. The state will be 128 * {@link State#SIGNED signed}. 129 * 130 * @param firstPart The first part, corresponding to the JWS header. 131 * Must not be {@code null}. 132 * @param secondPart The second part, corresponding to the payload. 133 * Must not be {@code null}. 134 * @param thirdPart The third part, corresponding to the signature. 135 * Must not be {@code null}. 136 * 137 * @throws ParseException If parsing of the serialised parts failed. 138 */ 139 public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart) 140 throws ParseException { 141 this(firstPart, new Payload(secondPart), thirdPart); 142 } 143 144 145 /** 146 * Creates a new signed JSON Web Signature (JWS) object with the 147 * specified serialised parts and payload which can be optionally 148 * unencoded (RFC 7797). The state will be {@link State#SIGNED signed}. 149 * 150 * @param firstPart The first part, corresponding to the JWS header. 151 * Must not be {@code null}. 152 * @param payload The payload. Must not be {@code null}. 153 * @param thirdPart The third part, corresponding to the signature. 154 * Must not be {@code null}. 155 * 156 * @throws ParseException If parsing of the serialised parts failed. 157 */ 158 public JWSObject(final Base64URL firstPart, final Payload payload, final Base64URL thirdPart) 159 throws ParseException { 160 161 if (firstPart == null) { 162 throw new IllegalArgumentException("The first part must not be null"); 163 } 164 try { 165 this.header = JWSHeader.parse(firstPart); 166 } catch (ParseException e) { 167 throw new ParseException("Invalid JWS header: " + e.getMessage(), 0); 168 } 169 170 if (payload == null) { 171 throw new IllegalArgumentException("The payload (second part) must not be null"); 172 } 173 setPayload(payload); 174 175 signingInputString = composeSigningInput(); 176 177 if (thirdPart == null) { 178 throw new IllegalArgumentException("The third part must not be null"); 179 } 180 if (thirdPart.toString().trim().isEmpty()) { 181 throw new ParseException("The signature must not be empty", 0); 182 } 183 184 signature = thirdPart; 185 state.set(State.SIGNED); // but signature not verified yet! 186 187 if (getHeader().isBase64URLEncodePayload()) { 188 setParsedParts(firstPart, payload.toBase64URL(), thirdPart); 189 } else { 190 setParsedParts(firstPart, new Base64URL(""), thirdPart); 191 } 192 } 193 194 @Override 195 public JWSHeader getHeader() { 196 197 return header; 198 } 199 200 201 /** 202 * Composes the signing input string from the header and payload. 203 * 204 * @return The signing input string. 205 */ 206 private String composeSigningInput() { 207 208 if (header.isBase64URLEncodePayload()) { 209 return getHeader().toBase64URL().toString() + '.' + getPayload().toBase64URL().toString(); 210 } else { 211 return getHeader().toBase64URL().toString() + '.' + getPayload().toString(); 212 } 213 } 214 215 216 /** 217 * Returns the signing input for this JWS object. 218 * 219 * @return The signing input, to be passed to a JWS signer or verifier. 220 */ 221 public byte[] getSigningInput() { 222 223 return signingInputString.getBytes(StandardCharset.UTF_8); 224 } 225 226 227 /** 228 * Returns the signature of this JWS object. 229 * 230 * @return The signature, {@code null} if the JWS object is not signed 231 * yet. 232 */ 233 public Base64URL getSignature() { 234 235 return signature; 236 } 237 238 239 /** 240 * Returns the state of the JWS secured object. 241 * 242 * @return The state. 243 */ 244 public State getState() { 245 246 return state.get(); 247 } 248 249 250 /** 251 * Ensures the current state is {@link State#UNSIGNED unsigned}. 252 * 253 * @throws IllegalStateException If the current state is not unsigned. 254 */ 255 private void ensureUnsignedState() { 256 257 if (state.get() != State.UNSIGNED) { 258 259 throw new IllegalStateException("The JWS object must be in an unsigned state"); 260 } 261 } 262 263 264 /** 265 * Ensures the current state is {@link State#SIGNED signed} or 266 * {@link State#VERIFIED verified}. 267 * 268 * @throws IllegalStateException If the current state is not signed or 269 * verified. 270 */ 271 private void ensureSignedOrVerifiedState() { 272 273 if (state.get() != State.SIGNED && state.get() != State.VERIFIED) { 274 275 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 276 } 277 } 278 279 280 /** 281 * Ensures the specified JWS signer supports the algorithm of this JWS 282 * object. 283 * 284 * @throws JOSEException If the JWS algorithm is not supported. 285 */ 286 private void ensureJWSSignerSupport(final JWSSigner signer) 287 throws JOSEException { 288 289 if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) { 290 291 throw new JOSEException("The " + getHeader().getAlgorithm() + 292 " algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms()); 293 } 294 } 295 296 297 /** 298 * Signs this JWS object with the specified signer. The JWS object must 299 * be in a {@link State#UNSIGNED unsigned} state. 300 * 301 * @param signer The JWS signer. Must not be {@code null}. 302 * 303 * @throws IllegalStateException If the JWS object is not in an 304 * {@link State#UNSIGNED unsigned state}. 305 * @throws JOSEException If the JWS object couldn't be signed. 306 */ 307 public synchronized void sign(final JWSSigner signer) 308 throws JOSEException { 309 310 ensureUnsignedState(); 311 312 ensureJWSSignerSupport(signer); 313 314 try { 315 signature = signer.sign(getHeader(), getSigningInput()); 316 317 } catch (final ActionRequiredForJWSCompletionException e) { 318 // Catch to enable state SIGNED update 319 throw new ActionRequiredForJWSCompletionException( 320 e.getMessage(), 321 e.getTriggeringOption(), 322 new CompletableJWSObjectSigning() { 323 @Override 324 public Base64URL complete() throws JOSEException { 325 signature = e.getCompletableJWSObjectSigning().complete(); 326 state.set(State.SIGNED); 327 return signature; 328 } 329 } 330 ); 331 332 } catch (JOSEException e) { 333 334 throw e; 335 336 } catch (Exception e) { 337 338 // Prevent throwing unchecked exceptions at this point, 339 // see issue #20 340 throw new JOSEException(e.getMessage(), e); 341 } 342 343 state.set(State.SIGNED); 344 } 345 346 347 /** 348 * Checks the signature of this JWS object with the specified verifier. 349 * The JWS object must be in a {@link State#SIGNED signed} state. 350 * 351 * @param verifier The JWS verifier. Must not be {@code null}. 352 * 353 * @return {@code true} if the signature was successfully verified, 354 * else {@code false}. 355 * 356 * @throws IllegalStateException If the JWS object is not in a 357 * {@link State#SIGNED signed} or 358 * {@link State#VERIFIED verified state}. 359 * @throws JOSEException If the JWS object couldn't be 360 * verified. 361 */ 362 public synchronized boolean verify(final JWSVerifier verifier) 363 throws JOSEException { 364 365 ensureSignedOrVerifiedState(); 366 367 boolean verified; 368 369 try { 370 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 371 372 } catch (JOSEException e) { 373 374 throw e; 375 376 } catch (Exception e) { 377 378 // Prevent throwing unchecked exceptions at this point, 379 // see issue #20 380 throw new JOSEException(e.getMessage(), e); 381 } 382 383 if (verified) { 384 385 state.set(State.VERIFIED); 386 } 387 388 return verified; 389 } 390 391 392 /** 393 * Serialises this JWS object to its compact format consisting of 394 * Base64URL-encoded parts delimited by period ('.') characters. It 395 * must be in a {@link State#SIGNED signed} or 396 * {@link State#VERIFIED verified} state. 397 * 398 * <pre> 399 * [header-base64url].[payload-base64url].[signature-base64url] 400 * </pre> 401 * 402 * @return The serialised JWS object. 403 * 404 * @throws IllegalStateException If the JWS object is not in a 405 * {@link State#SIGNED signed} or 406 * {@link State#VERIFIED verified} state. 407 */ 408 @Override 409 public String serialize() { 410 return serialize(false); 411 } 412 413 414 /** 415 * Serialises this JWS object to its compact format consisting of 416 * Base64URL-encoded parts delimited by period ('.') characters. It 417 * must be in a {@link State#SIGNED signed} or 418 * {@link State#VERIFIED verified} state. 419 * 420 * @param detachedPayload {@code true} to return a serialised object 421 * with a detached payload compliant with RFC 422 * 7797, {@code false} for regular JWS 423 * serialisation. 424 * 425 * @return The serialised JOSE object. 426 * 427 * @throws IllegalStateException If the JOSE object is not in a state 428 * that permits serialisation. 429 */ 430 public String serialize(final boolean detachedPayload) { 431 ensureSignedOrVerifiedState(); 432 433 if (detachedPayload) { 434 return header.toBase64URL().toString() + '.' + '.' + signature.toString(); 435 } 436 437 return signingInputString + '.' + signature.toString(); 438 } 439 440 441 /** 442 * Parses a JWS object from the specified string in compact format. The 443 * parsed JWS object will be given a {@link State#SIGNED} state. 444 * 445 * @param s The JWS string to parse. Must not be {@code null}. 446 * 447 * @return The JWS object. 448 * 449 * @throws ParseException If the string couldn't be parsed to a JWS 450 * object. 451 */ 452 public static JWSObject parse(final String s) 453 throws ParseException { 454 455 Base64URL[] parts = JOSEObject.split(s); 456 457 if (parts.length != 3) { 458 459 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 460 } 461 462 return new JWSObject(parts[0], parts[1], parts[2]); 463 } 464 465 466 /** 467 * Parses a JWS object from the specified string in compact format and 468 * a detached payload which can be optionally unencoded (RFC 7797). The 469 * parsed JWS object will be given a {@link State#SIGNED} state. 470 * 471 * @param s The JWS string to parse for a detached 472 * payload. Must not be {@code null}. 473 * @param detachedPayload The detached payload, optionally unencoded 474 * (RFC 7797). Must not be {@code null}. 475 * 476 * @return The JWS object. 477 * 478 * @throws ParseException If the string couldn't be parsed to a JWS 479 * object. 480 */ 481 public static JWSObject parse(final String s, final Payload detachedPayload) 482 throws ParseException { 483 484 Base64URL[] parts = JOSEObject.split(s); 485 486 if (parts.length != 3) { 487 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 488 } 489 490 if (! parts[1].toString().isEmpty()) { 491 throw new ParseException("The payload Base64URL part must be empty", 0); 492 } 493 494 return new JWSObject(parts[0], detachedPayload, parts[2]); 495 } 496}