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) secured object. This class is thread-safe. 014 * 015 * @author Vladimir Dzhuvinov 016 * @version 2016-04-13 017 */ 018@ThreadSafe 019public class JWSObject extends JOSEObject { 020 021 022 private static final long serialVersionUID = 1L; 023 024 025 /** 026 * Enumeration of the states of a JSON Web Signature (JWS) object. 027 */ 028 public 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 header. 052 */ 053 private final JWSHeader header; 054 055 056 /** 057 * The signing input for this JWS object. 058 * 059 * <p>Format: 060 * 061 * <pre> 062 * [header-base64url].[payload-base64url] 063 * </pre> 064 */ 065 private final String signingInputString; 066 067 068 /** 069 * The signature, {@code null} if not signed. 070 */ 071 private Base64URL signature; 072 073 074 /** 075 * The JWS object state. 076 */ 077 private State state; 078 079 080 /** 081 * Creates a new to-be-signed JSON Web Signature (JWS) object with the 082 * specified header and payload. The initial state will be 083 * {@link State#UNSIGNED unsigned}. 084 * 085 * @param header The JWS header. Must not be {@code null}. 086 * @param payload The payload. Must not be {@code null}. 087 */ 088 public JWSObject(final JWSHeader header, final Payload payload) { 089 090 if (header == null) { 091 092 throw new IllegalArgumentException("The JWS header must not be null"); 093 } 094 095 this.header = header; 096 097 if (payload == null) { 098 099 throw new IllegalArgumentException("The payload must not be null"); 100 } 101 102 setPayload(payload); 103 104 signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL()); 105 106 signature = null; 107 108 state = State.UNSIGNED; 109 } 110 111 112 /** 113 * Creates a new signed JSON Web Signature (JWS) object with the 114 * specified serialised parts. The state will be 115 * {@link State#SIGNED signed}. 116 * 117 * @param firstPart The first part, corresponding to the JWS header. 118 * Must not be {@code null}. 119 * @param secondPart The second part, corresponding to the payload. Must 120 * not be {@code null}. 121 * @param thirdPart The third part, corresponding to the signature. 122 * Must not be {@code null}. 123 * 124 * @throws ParseException If parsing of the serialised parts failed. 125 */ 126 public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart) 127 throws ParseException { 128 129 if (firstPart == null) { 130 131 throw new IllegalArgumentException("The first part must not be null"); 132 } 133 134 try { 135 this.header = JWSHeader.parse(firstPart); 136 137 } catch (ParseException e) { 138 139 throw new ParseException("Invalid JWS header: " + e.getMessage(), 0); 140 } 141 142 if (secondPart == null) { 143 144 throw new IllegalArgumentException("The second part must not be null"); 145 } 146 147 setPayload(new Payload(secondPart)); 148 149 signingInputString = composeSigningInput(firstPart, secondPart); 150 151 if (thirdPart == null) { 152 throw new IllegalArgumentException("The third part must not be null"); 153 } 154 155 signature = thirdPart; 156 157 state = State.SIGNED; // but signature not verified yet! 158 159 setParsedParts(firstPart, secondPart, thirdPart); 160 } 161 162 163 @Override 164 public JWSHeader getHeader() { 165 166 return header; 167 } 168 169 170 /** 171 * Composes the signing input for the specified JWS object parts. 172 * 173 * <p>Format: 174 * 175 * <pre> 176 * [header-base64url].[payload-base64url] 177 * </pre> 178 * 179 * @param firstPart The first part, corresponding to the JWS header. 180 * Must not be {@code null}. 181 * @param secondPart The second part, corresponding to the payload. 182 * Must not be {@code null}. 183 * 184 * @return The signing input string. 185 */ 186 private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) { 187 188 return firstPart.toString() + '.' + secondPart.toString(); 189 } 190 191 192 /** 193 * Returns 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 * Returns the signature of this JWS object. 211 * 212 * @return The signature, {@code null} if the JWS object is not signed 213 * yet. 214 */ 215 public Base64URL getSignature() { 216 217 return signature; 218 } 219 220 221 /** 222 * Returns the state of this JWS object. 223 * 224 * @return The state. 225 */ 226 public State getState() { 227 228 return state; 229 } 230 231 232 /** 233 * Ensures the current state is {@link State#UNSIGNED unsigned}. 234 * 235 * @throws IllegalStateException If the current state is not unsigned. 236 */ 237 private void ensureUnsignedState() { 238 239 if (state != State.UNSIGNED) { 240 241 throw new IllegalStateException("The JWS object must be in an unsigned state"); 242 } 243 } 244 245 246 /** 247 * Ensures the current state is {@link State#SIGNED signed} or 248 * {@link State#VERIFIED verified}. 249 * 250 * @throws IllegalStateException If the current state is not signed or 251 * verified. 252 */ 253 private void ensureSignedOrVerifiedState() { 254 255 if (state != State.SIGNED && state != State.VERIFIED) { 256 257 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 258 } 259 } 260 261 262 /** 263 * Ensures the specified JWS signer supports the algorithm of this JWS 264 * object. 265 * 266 * @throws JOSEException If the JWS algorithm is not supported. 267 */ 268 private void ensureJWSSignerSupport(final JWSSigner signer) 269 throws JOSEException { 270 271 if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) { 272 273 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 274 "\" algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms()); 275 } 276 } 277 278 279 /** 280 * Signs this JWS object with the specified signer. The JWS object must 281 * be in a {@link State#UNSIGNED unsigned} state. 282 * 283 * @param signer The JWS signer. Must not be {@code null}. 284 * 285 * @throws IllegalStateException If the JWS object is not in an 286 * {@link State#UNSIGNED unsigned state}. 287 * @throws JOSEException If the JWS object couldn't be signed. 288 */ 289 public synchronized void sign(final JWSSigner signer) 290 throws JOSEException { 291 292 ensureUnsignedState(); 293 294 ensureJWSSignerSupport(signer); 295 296 try { 297 signature = signer.sign(getHeader(), getSigningInput()); 298 299 } catch (JOSEException e) { 300 301 throw e; 302 303 } catch (Exception e) { 304 305 // Prevent throwing unchecked exceptions at this point, 306 // see issue #20 307 throw new JOSEException(e.getMessage(), e); 308 } 309 310 state = State.SIGNED; 311 } 312 313 314 /** 315 * Checks the signature of this JWS object with the specified verifier. 316 * The JWS object must be in a {@link State#SIGNED signed} state. 317 * 318 * @param verifier The JWS verifier. Must not be {@code null}. 319 * 320 * @return {@code true} if the signature was successfully verified, 321 * else {@code false}. 322 * 323 * @throws IllegalStateException If the JWS object is not in a 324 * {@link State#SIGNED signed} or 325 * {@link State#VERIFIED verified state}. 326 * @throws JOSEException If the JWS object couldn't be 327 * verified. 328 */ 329 public synchronized boolean verify(final JWSVerifier verifier) 330 throws JOSEException { 331 332 ensureSignedOrVerifiedState(); 333 334 boolean verified; 335 336 try { 337 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 338 339 } catch (JOSEException e) { 340 341 throw e; 342 343 } catch (Exception e) { 344 345 // Prevent throwing unchecked exceptions at this point, 346 // see issue #20 347 throw new JOSEException(e.getMessage(), e); 348 } 349 350 if (verified) { 351 352 state = State.VERIFIED; 353 } 354 355 return verified; 356 } 357 358 359 /** 360 * Serialises this JWS object to its compact format consisting of 361 * Base64URL-encoded parts delimited by period ('.') characters. It 362 * must be in a {@link State#SIGNED signed} or 363 * {@link State#VERIFIED verified} state. 364 * 365 * <pre> 366 * [header-base64url].[payload-base64url].[signature-base64url] 367 * </pre> 368 * 369 * @return The serialised JWS object. 370 * 371 * @throws IllegalStateException If the JWS object is not in a 372 * {@link State#SIGNED signed} or 373 * {@link State#VERIFIED verified} state. 374 */ 375 @Override 376 public String serialize() { 377 378 ensureSignedOrVerifiedState(); 379 380 return signingInputString + '.' + signature.toString(); 381 } 382 383 384 /** 385 * Parses a JWS object from the specified string in compact format. The 386 * parsed JWS object will be given a {@link State#SIGNED} state. 387 * 388 * @param s The string to parse. Must not be {@code null}. 389 * 390 * @return The JWS object. 391 * 392 * @throws ParseException If the string couldn't be parsed to a valid 393 * JWS object. 394 */ 395 public static JWSObject parse(final String s) 396 throws ParseException { 397 398 Base64URL[] parts = JOSEObject.split(s); 399 400 if (parts.length != 3) { 401 402 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 403 } 404 405 return new JWSObject(parts[0], parts[1], parts[2]); 406 } 407}