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 }