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