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