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 2021-10-05 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 signature = thirdPart; 181 state.set(State.SIGNED); // but signature not verified yet! 182 183 if (getHeader().isBase64URLEncodePayload()) { 184 setParsedParts(firstPart, payload.toBase64URL(), thirdPart); 185 } else { 186 setParsedParts(firstPart, new Base64URL(""), thirdPart); 187 } 188 } 189 190 @Override 191 public JWSHeader getHeader() { 192 193 return header; 194 } 195 196 197 /** 198 * Composes the signing input string from the header and payload. 199 * 200 * @return The signing input string. 201 */ 202 private String composeSigningInput() { 203 204 if (header.isBase64URLEncodePayload()) { 205 return getHeader().toBase64URL().toString() + '.' + getPayload().toBase64URL().toString(); 206 } else { 207 return getHeader().toBase64URL().toString() + '.' + getPayload().toString(); 208 } 209 } 210 211 212 /** 213 * Returns the signing input for this JWS object. 214 * 215 * @return The signing input, to be passed to a JWS signer or verifier. 216 */ 217 public byte[] getSigningInput() { 218 219 return signingInputString.getBytes(StandardCharset.UTF_8); 220 } 221 222 223 /** 224 * Returns the signature of this JWS object. 225 * 226 * @return The signature, {@code null} if the JWS object is not signed 227 * yet. 228 */ 229 public Base64URL getSignature() { 230 231 return signature; 232 } 233 234 235 /** 236 * Returns the state of the JWS secured object. 237 * 238 * @return The state. 239 */ 240 public State getState() { 241 242 return state.get(); 243 } 244 245 246 /** 247 * Ensures the current state is {@link State#UNSIGNED unsigned}. 248 * 249 * @throws IllegalStateException If the current state is not unsigned. 250 */ 251 private void ensureUnsignedState() { 252 253 if (state.get() != State.UNSIGNED) { 254 255 throw new IllegalStateException("The JWS object must be in an unsigned state"); 256 } 257 } 258 259 260 /** 261 * Ensures the current state is {@link State#SIGNED signed} or 262 * {@link State#VERIFIED verified}. 263 * 264 * @throws IllegalStateException If the current state is not signed or 265 * verified. 266 */ 267 private void ensureSignedOrVerifiedState() { 268 269 if (state.get() != State.SIGNED && state.get() != State.VERIFIED) { 270 271 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 272 } 273 } 274 275 276 /** 277 * Ensures the specified JWS signer supports the algorithm of this JWS 278 * object. 279 * 280 * @throws JOSEException If the JWS algorithm is not supported. 281 */ 282 private void ensureJWSSignerSupport(final JWSSigner signer) 283 throws JOSEException { 284 285 if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) { 286 287 throw new JOSEException("The " + getHeader().getAlgorithm() + 288 " algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms()); 289 } 290 } 291 292 293 /** 294 * Signs this JWS object with the specified signer. The JWS object must 295 * be in a {@link State#UNSIGNED unsigned} state. 296 * 297 * @param signer The JWS signer. Must not be {@code null}. 298 * 299 * @throws IllegalStateException If the JWS object is not in an 300 * {@link State#UNSIGNED unsigned state}. 301 * @throws JOSEException If the JWS object couldn't be signed. 302 */ 303 public synchronized void sign(final JWSSigner signer) 304 throws JOSEException { 305 306 ensureUnsignedState(); 307 308 ensureJWSSignerSupport(signer); 309 310 try { 311 signature = signer.sign(getHeader(), getSigningInput()); 312 313 } catch (final ActionRequiredForJWSCompletionException e) { 314 // Catch to enable state SIGNED update 315 throw new ActionRequiredForJWSCompletionException( 316 e.getMessage(), 317 e.getTriggeringOption(), 318 new CompletableJWSObjectSigning() { 319 @Override 320 public Base64URL complete() throws JOSEException { 321 signature = e.getCompletableJWSObjectSigning().complete(); 322 state.set(State.SIGNED); 323 return signature; 324 } 325 } 326 ); 327 328 } catch (JOSEException e) { 329 330 throw e; 331 332 } catch (Exception e) { 333 334 // Prevent throwing unchecked exceptions at this point, 335 // see issue #20 336 throw new JOSEException(e.getMessage(), e); 337 } 338 339 state.set(State.SIGNED); 340 } 341 342 343 /** 344 * Checks the signature of this JWS object with the specified verifier. 345 * The JWS object must be in a {@link State#SIGNED signed} state. 346 * 347 * @param verifier The JWS verifier. Must not be {@code null}. 348 * 349 * @return {@code true} if the signature was successfully verified, 350 * else {@code false}. 351 * 352 * @throws IllegalStateException If the JWS object is not in a 353 * {@link State#SIGNED signed} or 354 * {@link State#VERIFIED verified state}. 355 * @throws JOSEException If the JWS object couldn't be 356 * verified. 357 */ 358 public synchronized boolean verify(final JWSVerifier verifier) 359 throws JOSEException { 360 361 ensureSignedOrVerifiedState(); 362 363 boolean verified; 364 365 try { 366 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 367 368 } catch (JOSEException e) { 369 370 throw e; 371 372 } catch (Exception e) { 373 374 // Prevent throwing unchecked exceptions at this point, 375 // see issue #20 376 throw new JOSEException(e.getMessage(), e); 377 } 378 379 if (verified) { 380 381 state.set(State.VERIFIED); 382 } 383 384 return verified; 385 } 386 387 388 /** 389 * Serialises this JWS object to its compact format consisting of 390 * Base64URL-encoded parts delimited by period ('.') characters. It 391 * must be in a {@link State#SIGNED signed} or 392 * {@link State#VERIFIED verified} state. 393 * 394 * <pre> 395 * [header-base64url].[payload-base64url].[signature-base64url] 396 * </pre> 397 * 398 * @return The serialised JWS object. 399 * 400 * @throws IllegalStateException If the JWS object is not in a 401 * {@link State#SIGNED signed} or 402 * {@link State#VERIFIED verified} state. 403 */ 404 @Override 405 public String serialize() { 406 return serialize(false); 407 } 408 409 410 /** 411 * Serialises this JWS object to its compact format consisting of 412 * Base64URL-encoded parts delimited by period ('.') characters. It 413 * must be in a {@link State#SIGNED signed} or 414 * {@link State#VERIFIED verified} state. 415 * 416 * @param detachedPayload {@code true} to return a serialised object 417 * with a detached payload compliant with RFC 418 * 7797, {@code false} for regular JWS 419 * serialisation. 420 * 421 * @return The serialised JOSE object. 422 * 423 * @throws IllegalStateException If the JOSE object is not in a state 424 * that permits serialisation. 425 */ 426 public String serialize(final boolean detachedPayload) { 427 ensureSignedOrVerifiedState(); 428 429 if (detachedPayload) { 430 return header.toBase64URL().toString() + '.' + '.' + signature.toString(); 431 } 432 433 return signingInputString + '.' + signature.toString(); 434 } 435 436 437 /** 438 * Parses a JWS object from the specified string in compact format. The 439 * parsed JWS object will be given a {@link State#SIGNED} state. 440 * 441 * @param s The JWS string to parse. Must not be {@code null}. 442 * 443 * @return The JWS object. 444 * 445 * @throws ParseException If the string couldn't be parsed to a JWS 446 * object. 447 */ 448 public static JWSObject parse(final String s) 449 throws ParseException { 450 451 Base64URL[] parts = JOSEObject.split(s); 452 453 if (parts.length != 3) { 454 455 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 456 } 457 458 return new JWSObject(parts[0], parts[1], parts[2]); 459 } 460 461 462 /** 463 * Parses a JWS object from the specified string in compact format and 464 * a detached payload which can be optionally unencoded (RFC 7797). The 465 * parsed JWS object will be given a {@link State#SIGNED} state. 466 * 467 * @param s The JWS string to parse for a detached 468 * payload. Must not be {@code null}. 469 * @param detachedPayload The detached payload, optionally unencoded 470 * (RFC 7797). Must not be {@code null}. 471 * 472 * @return The JWS object. 473 * 474 * @throws ParseException If the string couldn't be parsed to a JWS 475 * object. 476 */ 477 public static JWSObject parse(final String s, final Payload detachedPayload) 478 throws ParseException { 479 480 Base64URL[] parts = JOSEObject.split(s); 481 482 if (parts.length != 3) { 483 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 484 } 485 486 if (! parts[1].toString().isEmpty()) { 487 throw new ParseException("The payload Base64URL part must be empty", 0); 488 } 489 490 return new JWSObject(parts[0], detachedPayload, parts[2]); 491 } 492}