001 package com.nimbusds.jose; 002 003 004 import java.io.UnsupportedEncodingException; 005 import java.text.ParseException; 006 007 import net.jcip.annotations.ThreadSafe; 008 009 import 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$ (2013-03-27) 017 */ 018 @ThreadSafe 019 public 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 JWSHeader header; 051 052 053 /** 054 * The signable content of this JWS object. 055 * 056 * <p>Format: 057 * 058 * <pre> 059 * [header-base64url].[payload-base64url] 060 * </pre> 061 */ 062 private byte[] signableContent; 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 JWSHeader 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 setSignableContent(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 setSignableContent(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 * Sets the signable content of this JWS object. 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. Must 179 * not be {@code null}. 180 */ 181 private void setSignableContent(final Base64URL firstPart, final Base64URL secondPart) { 182 183 StringBuilder sb = new StringBuilder(firstPart.toString()); 184 sb.append('.'); 185 sb.append(secondPart.toString()); 186 187 try { 188 signableContent = sb.toString().getBytes("UTF-8"); 189 190 } catch (UnsupportedEncodingException e) { 191 192 // UTF-8 should always be supported 193 } 194 } 195 196 197 /** 198 * Gets the signable content of this JWS object. 199 * 200 * <p>Format: 201 * 202 * <pre> 203 * [header-base64url].[payload-base64url] 204 * </pre> 205 * 206 * @return The signable content, ready for passing to the signing or 207 * verification service. 208 */ 209 public byte[] getSignableContent() { 210 211 return signableContent; 212 } 213 214 215 /** 216 * Gets the signature of this JWS object. 217 * 218 * @return The signature, {@code null} if the JWS object is not signed 219 * yet. 220 */ 221 public Base64URL getSignature() { 222 223 return signature; 224 } 225 226 227 /** 228 * Gets the state of this JWS object. 229 * 230 * @return The state. 231 */ 232 public State getState() { 233 234 return state; 235 } 236 237 238 /** 239 * Ensures the current state is {@link State#UNSIGNED unsigned}. 240 * 241 * @throws IllegalStateException If the current state is not unsigned. 242 */ 243 private void ensureUnsignedState() { 244 245 if (state != State.UNSIGNED) { 246 247 throw new IllegalStateException("The JWS object must be in an unsigned state"); 248 } 249 } 250 251 252 /** 253 * Ensures the current state is {@link State#SIGNED signed} or 254 * {@link State#VERIFIED verified}. 255 * 256 * @throws IllegalStateException If the current state is not signed or 257 * verified. 258 */ 259 private void ensureSignedOrVerifiedState() { 260 261 if (state != State.SIGNED && state != State.VERIFIED) { 262 263 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 264 } 265 } 266 267 268 /** 269 * Ensures the specified JWS signer supports the algorithm of this JWS 270 * object. 271 * 272 * @throws JOSEException If the JWS algorithm is not supported. 273 */ 274 private void ensureJWSSignerSupport(final JWSSigner signer) 275 throws JOSEException { 276 277 if (! signer.supportedAlgorithms().contains(getHeader().getAlgorithm())) { 278 279 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 280 "\" algorithm is not supported by the JWS signer"); 281 } 282 } 283 284 285 /** 286 * Ensures the specified JWS verifier accepts the algorithm and the headers 287 * of this JWS object. 288 * 289 * @throws JOSEException If the JWS algorithm or headers are not accepted. 290 */ 291 private void ensureJWSVerifierAcceptance(final JWSVerifier verifier) 292 throws JOSEException { 293 294 JWSHeaderFilter filter = verifier.getJWSHeaderFilter(); 295 296 if (filter == null) { 297 return; 298 } 299 300 if (! filter.getAcceptedAlgorithms().contains(getHeader().getAlgorithm())) { 301 302 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 303 "\" algorithm is not accepted by the JWS verifier"); 304 } 305 306 307 if (! filter.getAcceptedParameters().containsAll(getHeader().getIncludedParameters())) { 308 309 throw new JOSEException("One or more header parameters not accepted by the JWS verifier"); 310 } 311 } 312 313 314 /** 315 * Signs this JWS object with the specified signer. The JWS object must 316 * be in a {@link State#UNSIGNED unsigned} state. 317 * 318 * @param signer The JWS signer. Must not be {@code null}. 319 * 320 * @throws IllegalStateException If the JWS object is not in an 321 * {@link State#UNSIGNED unsigned state}. 322 * @throws JOSEException If the JWS object couldn't be signed. 323 */ 324 public synchronized void sign(final JWSSigner signer) 325 throws JOSEException { 326 327 ensureUnsignedState(); 328 329 ensureJWSSignerSupport(signer); 330 331 try { 332 signature = signer.sign(getHeader(), getSignableContent()); 333 334 } catch (JOSEException e) { 335 336 throw e; 337 338 } catch (Exception e) { 339 340 // Prevent throwing unchecked exceptions at this point, 341 // see issue #20 342 throw new JOSEException(e.getMessage(), e); 343 } 344 345 346 state = State.SIGNED; 347 } 348 349 350 /** 351 * Checks the signature of this JWS object with the specified verifier. 352 * The JWS object must be in a {@link State#SIGNED signed} state. 353 * 354 * @param verifier The JWS verifier. Must not be {@code null}. 355 * 356 * @return {@code true} if the signature was successfully verified, 357 * else {@code false}. 358 * 359 * @throws IllegalStateException If the JWS object is not in a 360 * {@link State#SIGNED signed} or 361 * {@link State#VERIFIED verified state}. 362 * @throws JOSEException If the JWS object couldn't be verified. 363 */ 364 public synchronized boolean verify(final JWSVerifier verifier) 365 throws JOSEException { 366 367 ensureSignedOrVerifiedState(); 368 369 ensureJWSVerifierAcceptance(verifier); 370 371 boolean verified = false; 372 373 try { 374 verified = verifier.verify(getHeader(), getSignableContent(), getSignature()); 375 376 } catch (JOSEException e) { 377 378 throw e; 379 380 } catch (Exception e) { 381 382 // Prevent throwing unchecked exceptions at this point, 383 // see issue #20 384 throw new JOSEException(e.getMessage(), e); 385 } 386 387 if (verified) { 388 389 state = State.VERIFIED; 390 } 391 392 return verified; 393 } 394 395 396 /** 397 * Serialises this JWS object to its compact format consisting of 398 * Base64URL-encoded parts delimited by period ('.') characters. It 399 * must be in a {@link State#SIGNED signed} or 400 * {@link State#VERIFIED verified} state. 401 * 402 * <pre> 403 * [header-base64url].[payload-base64url].[signature-base64url] 404 * </pre> 405 * 406 * @return The serialised JWS object. 407 * 408 * @throws IllegalStateException If the JWS object is not in a 409 * {@link State#SIGNED signed} or 410 * {@link State#VERIFIED verified} state. 411 */ 412 @Override 413 public String serialize() { 414 415 ensureSignedOrVerifiedState(); 416 417 StringBuilder sb = new StringBuilder(header.toBase64URL().toString()); 418 sb.append('.'); 419 sb.append(getPayload().toBase64URL().toString()); 420 sb.append('.'); 421 sb.append(signature.toString()); 422 return sb.toString(); 423 } 424 425 426 /** 427 * Parses a JWS object from the specified string in compact format. The 428 * parsed JWS object will be given a {@link State#SIGNED} state. 429 * 430 * @param s The string to parse. Must not be {@code null}. 431 * 432 * @return The JWS object. 433 * 434 * @throws ParseException If the string couldn't be parsed to a valid 435 * JWS object. 436 */ 437 public static JWSObject parse(final String s) 438 throws ParseException { 439 440 Base64URL[] parts = JOSEObject.split(s); 441 442 if (parts.length != 3) { 443 444 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 445 } 446 447 return new JWSObject(parts[0], parts[1], parts[2]); 448 } 449 }