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 com.nimbusds.jose.util.Base64URL; 022import com.nimbusds.jose.util.StandardCharset; 023import net.jcip.annotations.ThreadSafe; 024 025import java.security.Signature; 026import java.text.ParseException; 027import java.util.concurrent.atomic.AtomicReference; 028 029 030/** 031 * JSON Web Signature (JWS) secured object with 032 * <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-7.1">compact 033 * serialisation</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 324 @Override 325 public Signature getInitializedSignature() { 326 return e.getCompletableJWSObjectSigning().getInitializedSignature(); 327 } 328 329 @Override 330 public Base64URL complete() throws JOSEException { 331 signature = e.getCompletableJWSObjectSigning().complete(); 332 state.set(State.SIGNED); 333 return signature; 334 } 335 } 336 ); 337 338 } catch (JOSEException e) { 339 340 throw e; 341 342 } catch (Exception e) { 343 344 // Prevent throwing unchecked exceptions at this point, 345 // see issue #20 346 throw new JOSEException(e.getMessage(), e); 347 } 348 349 state.set(State.SIGNED); 350 } 351 352 353 /** 354 * Checks the signature of this JWS object with the specified verifier. 355 * The JWS object must be in a {@link State#SIGNED signed} state. 356 * 357 * @param verifier The JWS verifier. Must not be {@code null}. 358 * 359 * @return {@code true} if the signature was successfully verified, 360 * else {@code false}. 361 * 362 * @throws IllegalStateException If the JWS object is not in a 363 * {@link State#SIGNED signed} or 364 * {@link State#VERIFIED verified state}. 365 * @throws JOSEException If the JWS object couldn't be 366 * verified. 367 */ 368 public synchronized boolean verify(final JWSVerifier verifier) 369 throws JOSEException { 370 371 ensureSignedOrVerifiedState(); 372 373 boolean verified; 374 375 try { 376 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 377 378 } catch (JOSEException e) { 379 380 throw e; 381 382 } catch (Exception e) { 383 384 // Prevent throwing unchecked exceptions at this point, 385 // see issue #20 386 throw new JOSEException(e.getMessage(), e); 387 } 388 389 if (verified) { 390 391 state.set(State.VERIFIED); 392 } 393 394 return verified; 395 } 396 397 398 /** 399 * Serialises this JWS object to its compact format consisting of 400 * Base64URL-encoded parts delimited by period ('.') characters. It 401 * must be in a {@link State#SIGNED signed} or 402 * {@link State#VERIFIED verified} state. 403 * 404 * <pre> 405 * [header-base64url].[payload-base64url].[signature-base64url] 406 * </pre> 407 * 408 * @return The serialised JWS object. 409 * 410 * @throws IllegalStateException If the JWS object is not in a 411 * {@link State#SIGNED signed} or 412 * {@link State#VERIFIED verified} state. 413 */ 414 @Override 415 public String serialize() { 416 return serialize(false); 417 } 418 419 420 /** 421 * Serialises this JWS object to its compact format consisting of 422 * Base64URL-encoded parts delimited by period ('.') characters. It 423 * must be in a {@link State#SIGNED signed} or 424 * {@link State#VERIFIED verified} state. 425 * 426 * @param detachedPayload {@code true} to return a serialised object 427 * with a detached payload compliant with RFC 428 * 7797, {@code false} for regular JWS 429 * serialisation. 430 * 431 * @return The serialised JOSE object. 432 * 433 * @throws IllegalStateException If the JOSE object is not in a state 434 * that permits serialisation. 435 */ 436 public String serialize(final boolean detachedPayload) { 437 ensureSignedOrVerifiedState(); 438 439 if (detachedPayload) { 440 return header.toBase64URL().toString() + '.' + '.' + signature.toString(); 441 } 442 443 return signingInputString + '.' + signature.toString(); 444 } 445 446 447 /** 448 * Parses a JWS object from the specified string in compact format. The 449 * parsed JWS object will be given a {@link State#SIGNED} state. 450 * 451 * @param s The JWS string to parse. Must not be {@code null}. 452 * 453 * @return The JWS object. 454 * 455 * @throws ParseException If the string couldn't be parsed to a JWS 456 * object. 457 */ 458 public static JWSObject parse(final String s) 459 throws ParseException { 460 461 Base64URL[] parts = JOSEObject.split(s); 462 463 if (parts.length != 3) { 464 465 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 466 } 467 468 return new JWSObject(parts[0], parts[1], parts[2]); 469 } 470 471 472 /** 473 * Parses a JWS object from the specified string in compact format and 474 * a detached payload which can be optionally unencoded (RFC 7797). The 475 * parsed JWS object will be given a {@link State#SIGNED} state. 476 * 477 * @param s The JWS string to parse for a detached 478 * payload. Must not be {@code null}. 479 * @param detachedPayload The detached payload, optionally unencoded 480 * (RFC 7797). Must not be {@code null}. 481 * 482 * @return The JWS object. 483 * 484 * @throws ParseException If the string couldn't be parsed to a JWS 485 * object. 486 */ 487 public static JWSObject parse(final String s, final Payload detachedPayload) 488 throws ParseException { 489 490 Base64URL[] parts = JOSEObject.split(s); 491 492 if (parts.length != 3) { 493 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 494 } 495 496 if (! parts[1].toString().isEmpty()) { 497 throw new ParseException("The payload Base64URL part must be empty", 0); 498 } 499 500 return new JWSObject(parts[0], detachedPayload, parts[2]); 501 } 502}