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