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