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