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$ (2012-10-23) 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 throw new IllegalArgumentException("The JWS header must not be null"); 089 } 090 091 this.header = header; 092 093 if (payload == null) { 094 throw new IllegalArgumentException("The payload must not be null"); 095 } 096 097 setPayload(payload); 098 099 setSignableContent(header.toBase64URL(), payload.toBase64URL()); 100 101 signature = null; 102 103 state = State.UNSIGNED; 104 } 105 106 107 /** 108 * Creates a new signed JSON Web Signature (JWS) object with the 109 * specified serialised parts. The state will be 110 * {@link State#SIGNED signed}. 111 * 112 * @param firstPart The first part, corresponding to the JWS header. 113 * Must not be {@code null}. 114 * @param secondPart The second part, corresponding to the payload. Must 115 * not be {@code null}. 116 * @param thirdPart The third part, corresponding to the signature. 117 * Must not be {@code null}. 118 * 119 * @throws ParseException If parsing of the serialised parts failed. 120 */ 121 public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart) 122 throws ParseException { 123 124 if (firstPart == null) { 125 throw new IllegalArgumentException("The first part must not be null"); 126 } 127 128 try { 129 this.header = JWSHeader.parse(firstPart); 130 131 } catch (ParseException e) { 132 133 throw new ParseException("Invalid JWS header: " + e.getMessage(), 0); 134 } 135 136 if (secondPart == null) { 137 throw new IllegalArgumentException("The second part must not be null"); 138 } 139 140 setPayload(new Payload(secondPart)); 141 142 setSignableContent(firstPart, secondPart); 143 144 if (thirdPart == null) { 145 throw new IllegalArgumentException("The third part must not be null"); 146 } 147 148 signature = thirdPart; 149 150 state = State.SIGNED; // but signature not verified yet! 151 152 setParsedParts(firstPart, secondPart, thirdPart); 153 } 154 155 156 @Override 157 public ReadOnlyJWSHeader getHeader() { 158 159 return header; 160 } 161 162 163 /** 164 * Sets the signable content of this JWS object. 165 * 166 * <p>Format: 167 * 168 * <pre> 169 * [header-base64url].[payload-base64url] 170 * </pre> 171 * 172 * @param firstPart The first part, corresponding to the JWS header. 173 * Must not be {@code null}. 174 * @param secondPart The second part, corresponding to the payload. Must 175 * not be {@code null}. 176 */ 177 private void setSignableContent(final Base64URL firstPart, final Base64URL secondPart) { 178 179 StringBuilder sb = new StringBuilder(firstPart.toString()); 180 sb.append('.'); 181 sb.append(secondPart.toString()); 182 183 try { 184 signableContent = sb.toString().getBytes("UTF-8"); 185 186 } catch (UnsupportedEncodingException e) { 187 188 // UTF-8 should always be supported 189 } 190 } 191 192 193 /** 194 * Gets the signable content of this JWS object. 195 * 196 * <p>Format: 197 * 198 * <pre> 199 * [header-base64url].[payload-base64url] 200 * </pre> 201 * 202 * @return The signable content, ready for passing to the signing or 203 * verification service. 204 */ 205 public byte[] getSignableContent() { 206 207 return signableContent; 208 } 209 210 211 /** 212 * Gets the signature of this JWS object. 213 * 214 * @return The signature, {@code null} if the JWS object is not signed 215 * yet. 216 */ 217 public Base64URL getSignature() { 218 219 return signature; 220 } 221 222 223 /** 224 * Gets the state of this JWS object. 225 * 226 * @return The state. 227 */ 228 public State getState() { 229 230 return state; 231 } 232 233 234 /** 235 * Ensures the current state is {@link State#UNSIGNED unsigned}. 236 * 237 * @throws IllegalStateException If the current state is not unsigned. 238 */ 239 private void ensureUnsignedState() { 240 241 if (state != State.UNSIGNED) { 242 throw new IllegalStateException("The JWS object must be in an unsigned state"); 243 } 244 } 245 246 247 /** 248 * Ensures the current state is {@link State#SIGNED signed} or 249 * {@link State#VERIFIED verified}. 250 * 251 * @throws IllegalStateException If the current state is not signed or 252 * verified. 253 */ 254 private void ensureSignedOrVerifiedState() { 255 256 if (state != State.SIGNED && state != State.VERIFIED) { 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.supportedAlgorithms().contains(getHeader().getAlgorithm())) { 272 273 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 274 "\" algorithm is not supported by the JWS signer"); 275 } 276 } 277 278 279 /** 280 * Ensures the specified JWS verifier accepts the algorithm and the headers 281 * of this JWS object. 282 * 283 * @throws JOSEException If the JWS algorithm or headers are not accepted. 284 */ 285 private void ensureJWSVerifierAcceptance(final JWSVerifier verifier) 286 throws JOSEException { 287 288 JWSHeaderFilter filter = verifier.getJWSHeaderFilter(); 289 290 if (filter == null) { 291 return; 292 } 293 294 if (! filter.getAcceptedAlgorithms().contains(getHeader().getAlgorithm())) { 295 296 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 297 "\" algorithm is not accepted by the JWS verifier"); 298 } 299 300 301 if (! filter.getAcceptedParameters().containsAll(getHeader().getIncludedParameters())) { 302 303 throw new JOSEException("One or more header parameters not accepted by the JWS verifier"); 304 } 305 } 306 307 308 /** 309 * Signs this JWS object with the specified signer. The JWS object must 310 * be in a {@link State#UNSIGNED unsigned} state. 311 * 312 * @param signer The JWS signer. Must not be {@code null}. 313 * 314 * @throws IllegalStateException If the JWS object is not in an 315 * {@link State#UNSIGNED unsigned state}. 316 * @throws JOSEException If the JWS object couldn't be signed. 317 */ 318 public synchronized void sign(final JWSSigner signer) 319 throws JOSEException { 320 321 ensureUnsignedState(); 322 323 ensureJWSSignerSupport(signer); 324 325 signature = signer.sign(getHeader(), getSignableContent()); 326 327 state = State.SIGNED; 328 } 329 330 331 /** 332 * Checks the signature of this JWS object with the specified verifier. The 333 * JWS object must be in a {@link State#SIGNED signed} state. 334 * 335 * @param verifier The JWS verifier. Must not be {@code null}. 336 * 337 * @return {@code true} if the signature was successfully verified, else 338 * {@code false}. 339 * 340 * @throws IllegalStateException If the JWS object is not in a 341 * {@link State#SIGNED signed} or 342 * {@link State#VERIFIED verified state}. 343 * @throws JOSEException If the JWS object couldn't be verified. 344 */ 345 public synchronized boolean verify(final JWSVerifier verifier) 346 throws JOSEException { 347 348 ensureSignedOrVerifiedState(); 349 350 ensureJWSVerifierAcceptance(verifier); 351 352 boolean verified = verifier.verify(getHeader(), getSignableContent(), getSignature()); 353 354 if (verified) { 355 state = State.VERIFIED; 356 } 357 358 return verified; 359 } 360 361 362 /** 363 * Serialises this JWS object to its compact format consisting of 364 * Base64URL-encoded parts delimited by period ('.') characters. It must 365 * be in a {@link State#SIGNED signed} or {@link State#VERIFIED verified} 366 * state. 367 * 368 * <pre> 369 * [header-base64url].[payload-base64url].[signature-base64url] 370 * </pre> 371 * 372 * @return The serialised JWS object. 373 * 374 * @throws IllegalStateException If the JWS object is not in a 375 * {@link State#SIGNED signed} or 376 * {@link State#VERIFIED verified} state. 377 */ 378 @Override 379 public String serialize() { 380 381 ensureSignedOrVerifiedState(); 382 383 StringBuilder sb = new StringBuilder(header.toBase64URL().toString()); 384 sb.append('.'); 385 sb.append(getPayload().toBase64URL().toString()); 386 sb.append('.'); 387 sb.append(signature.toString()); 388 return sb.toString(); 389 } 390 391 392 /** 393 * Parses a JWS object from the specified string in compact format. The 394 * parsed JWS object will be given a {@link State#SIGNED} state. 395 * 396 * @param s The string to parse. Must not be {@code null}. 397 * 398 * @return The JWS object. 399 * 400 * @throws ParseException If the string couldn't be parsed to a valid JWS 401 * object. 402 */ 403 public static JWSObject parse(String s) 404 throws ParseException { 405 406 Base64URL[] parts = JOSEObject.split(s); 407 408 if (parts.length != 3) { 409 throw new ParseException("Unexpected number of Base64URL parts, must be three", 0); 410 } 411 412 return new JWSObject(parts[0], parts[1], parts[2]); 413 } 414 }