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