001package com.nimbusds.jose; 002 003 004import java.nio.charset.Charset; 005import java.text.ParseException; 006 007import net.jcip.annotations.ThreadSafe; 008 009import com.nimbusds.jose.util.Base64URL; 010 011 012/** 013 * JSON Web Signature (JWS) object. This class is thread-safe. 014 * 015 * @author Vladimir Dzhuvinov 016 * @version $version$ (2014-04-20) 017 */ 018@ThreadSafe 019public class JWSObject extends JOSEObject { 020 021 022 /** 023 * Enumeration of the states of a JSON Web Signature (JWS) object. 024 */ 025 public static enum State { 026 027 028 /** 029 * The JWS object is created but not signed yet. 030 */ 031 UNSIGNED, 032 033 034 /** 035 * The JWS object is signed but its signature is not verified. 036 */ 037 SIGNED, 038 039 040 /** 041 * The JWS object is signed and its signature was successfully verified. 042 */ 043 VERIFIED; 044 } 045 046 047 /** 048 * The header. 049 */ 050 private final ReadOnlyJWSHeader header; 051 052 053 /** 054 * The signing input for this JWS object. 055 * 056 * <p>Format: 057 * 058 * <pre> 059 * [header-base64url].[payload-base64url] 060 * </pre> 061 */ 062 private final String signingInputString; 063 064 065 /** 066 * The signature, {@code null} if not signed. 067 */ 068 private Base64URL signature; 069 070 071 /** 072 * The JWS object state. 073 */ 074 private State state; 075 076 077 /** 078 * Creates a new to-be-signed JSON Web Signature (JWS) object with the 079 * specified header and payload. The initial state will be 080 * {@link State#UNSIGNED unsigned}. 081 * 082 * @param header The JWS header. Must not be {@code null}. 083 * @param payload The payload. Must not be {@code null}. 084 */ 085 public JWSObject(final ReadOnlyJWSHeader header, final Payload payload) { 086 087 if (header == null) { 088 089 throw new IllegalArgumentException("The JWS header must not be null"); 090 } 091 092 this.header = header; 093 094 if (payload == null) { 095 096 throw new IllegalArgumentException("The payload must not be null"); 097 } 098 099 setPayload(payload); 100 101 signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL()); 102 103 signature = null; 104 105 state = State.UNSIGNED; 106 } 107 108 109 /** 110 * Creates a new signed JSON Web Signature (JWS) object with the 111 * specified serialised parts. The state will be 112 * {@link State#SIGNED signed}. 113 * 114 * @param firstPart The first part, corresponding to the JWS header. 115 * Must not be {@code null}. 116 * @param secondPart The second part, corresponding to the payload. Must 117 * not be {@code null}. 118 * @param thirdPart The third part, corresponding to the signature. 119 * Must not be {@code null}. 120 * 121 * @throws ParseException If parsing of the serialised parts failed. 122 */ 123 public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart) 124 throws ParseException { 125 126 if (firstPart == null) { 127 128 throw new IllegalArgumentException("The first part must not be null"); 129 } 130 131 try { 132 this.header = JWSHeader.parse(firstPart); 133 134 } catch (ParseException e) { 135 136 throw new ParseException("Invalid JWS header: " + e.getMessage(), 0); 137 } 138 139 if (secondPart == null) { 140 141 throw new IllegalArgumentException("The second part must not be null"); 142 } 143 144 setPayload(new Payload(secondPart)); 145 146 signingInputString = composeSigningInput(firstPart, secondPart); 147 148 if (thirdPart == null) { 149 throw new IllegalArgumentException("The third part must not be null"); 150 } 151 152 signature = thirdPart; 153 154 state = State.SIGNED; // but signature not verified yet! 155 156 setParsedParts(firstPart, secondPart, thirdPart); 157 } 158 159 160 @Override 161 public ReadOnlyJWSHeader getHeader() { 162 163 return header; 164 } 165 166 167 /** 168 * Composes the signing input for the specified JWS object parts. 169 * 170 * <p>Format: 171 * 172 * <pre> 173 * [header-base64url].[payload-base64url] 174 * </pre> 175 * 176 * @param firstPart The first part, corresponding to the JWS header. 177 * Must not be {@code null}. 178 * @param secondPart The second part, corresponding to the payload. 179 * Must not be {@code null}. 180 * 181 * @return The signing input string. 182 */ 183 private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) { 184 185 StringBuilder sb = new StringBuilder(firstPart.toString()); 186 sb.append('.'); 187 sb.append(secondPart.toString()); 188 return sb.toString(); 189 } 190 191 192 /** 193 * Gets the signing input for this JWS object. 194 * 195 * <p>Format: 196 * 197 * <pre> 198 * [header-base64url].[payload-base64url] 199 * </pre> 200 * 201 * @return The signing input, to be passed to a JWS signer or verifier. 202 */ 203 public byte[] getSigningInput() { 204 205 return signingInputString.getBytes(Charset.forName("UTF-8")); 206 } 207 208 209 /** 210 * @deprecated Use {@link #getSigningInput} instead. 211 */ 212 @Deprecated 213 public byte[] getSignableContent() { 214 215 return getSigningInput(); 216 } 217 218 219 /** 220 * Gets the signature of this JWS object. 221 * 222 * @return The signature, {@code null} if the JWS object is not signed 223 * yet. 224 */ 225 public Base64URL getSignature() { 226 227 return signature; 228 } 229 230 231 /** 232 * Gets the state of this JWS object. 233 * 234 * @return The state. 235 */ 236 public State getState() { 237 238 return state; 239 } 240 241 242 /** 243 * Ensures the current state is {@link State#UNSIGNED unsigned}. 244 * 245 * @throws IllegalStateException If the current state is not unsigned. 246 */ 247 private void ensureUnsignedState() { 248 249 if (state != State.UNSIGNED) { 250 251 throw new IllegalStateException("The JWS object must be in an unsigned state"); 252 } 253 } 254 255 256 /** 257 * Ensures the current state is {@link State#SIGNED signed} or 258 * {@link State#VERIFIED verified}. 259 * 260 * @throws IllegalStateException If the current state is not signed or 261 * verified. 262 */ 263 private void ensureSignedOrVerifiedState() { 264 265 if (state != State.SIGNED && state != State.VERIFIED) { 266 267 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 268 } 269 } 270 271 272 /** 273 * Ensures the specified JWS signer supports the algorithm of this JWS 274 * object. 275 * 276 * @throws JOSEException If the JWS algorithm is not supported. 277 */ 278 private void ensureJWSSignerSupport(final JWSSigner signer) 279 throws JOSEException { 280 281 if (! signer.supportedAlgorithms().contains(getHeader().getAlgorithm())) { 282 283 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 284 "\" algorithm is not supported by the JWS signer"); 285 } 286 } 287 288 289 /** 290 * Ensures the specified JWS verifier accepts the algorithm and the headers 291 * of this JWS object. 292 * 293 * @throws JOSEException If the JWS algorithm or headers are not accepted. 294 */ 295 private void ensureJWSVerifierAcceptance(final JWSVerifier verifier) 296 throws JOSEException { 297 298 if (! verifier.getAcceptedAlgorithms().contains(getHeader().getAlgorithm())) { 299 300 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 301 "\" algorithm is not accepted by the JWS verifier"); 302 } 303 } 304 305 306 /** 307 * Signs this JWS object with the specified signer. The JWS object must 308 * be in a {@link State#UNSIGNED unsigned} state. 309 * 310 * @param signer The JWS signer. Must not be {@code null}. 311 * 312 * @throws IllegalStateException If the JWS object is not in an 313 * {@link State#UNSIGNED unsigned state}. 314 * @throws JOSEException If the JWS object couldn't be signed. 315 */ 316 public synchronized void sign(final JWSSigner signer) 317 throws JOSEException { 318 319 ensureUnsignedState(); 320 321 ensureJWSSignerSupport(signer); 322 323 try { 324 signature = signer.sign(getHeader(), getSigningInput()); 325 326 } catch (JOSEException e) { 327 328 throw e; 329 330 } catch (Exception e) { 331 332 // Prevent throwing unchecked exceptions at this point, 333 // see issue #20 334 throw new JOSEException(e.getMessage(), e); 335 } 336 337 state = State.SIGNED; 338 } 339 340 341 /** 342 * Checks the signature of this JWS object with the specified verifier. 343 * The JWS object must be in a {@link State#SIGNED signed} state. 344 * 345 * @param verifier The JWS verifier. Must not be {@code null}. 346 * 347 * @return {@code true} if the signature was successfully verified, 348 * else {@code false}. 349 * 350 * @throws IllegalStateException If the JWS object is not in a 351 * {@link State#SIGNED signed} or 352 * {@link State#VERIFIED verified state}. 353 * @throws JOSEException If the JWS object couldn't be verified. 354 */ 355 public synchronized boolean verify(final JWSVerifier verifier) 356 throws JOSEException { 357 358 ensureSignedOrVerifiedState(); 359 360 ensureJWSVerifierAcceptance(verifier); 361 362 boolean verified; 363 364 try { 365 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 366 367 } catch (JOSEException e) { 368 369 throw e; 370 371 } catch (Exception e) { 372 373 // Prevent throwing unchecked exceptions at this point, 374 // see issue #20 375 throw new JOSEException(e.getMessage(), e); 376 } 377 378 if (verified) { 379 380 state = State.VERIFIED; 381 } 382 383 return verified; 384 } 385 386 387 /** 388 * Serialises this JWS object to its compact format consisting of 389 * Base64URL-encoded parts delimited by period ('.') characters. It 390 * must be in a {@link State#SIGNED signed} or 391 * {@link State#VERIFIED verified} state. 392 * 393 * <pre> 394 * [header-base64url].[payload-base64url].[signature-base64url] 395 * </pre> 396 * 397 * @return The serialised JWS object. 398 * 399 * @throws IllegalStateException If the JWS object is not in a 400 * {@link State#SIGNED signed} or 401 * {@link State#VERIFIED verified} state. 402 */ 403 @Override 404 public String serialize() { 405 406 ensureSignedOrVerifiedState(); 407 408 StringBuilder sb = new StringBuilder(signingInputString); 409 sb.append('.'); 410 sb.append(signature.toString()); 411 return sb.toString(); 412 } 413 414 415 /** 416 * Parses a JWS object from the specified string in compact format. The 417 * parsed JWS object will be given a {@link State#SIGNED} state. 418 * 419 * @param s The string to parse. Must not be {@code null}. 420 * 421 * @return The JWS object. 422 * 423 * @throws ParseException If the string couldn't be parsed to a valid 424 * JWS object. 425 */ 426 public static JWSObject parse(final String s) 427 throws ParseException { 428 429 Base64URL[] parts = JOSEObject.split(s); 430 431 if (parts.length != 3) { 432 433 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 434 } 435 436 return new JWSObject(parts[0], parts[1], parts[2]); 437 } 438}