001package com.nimbusds.jose;
002
003
004import java.nio.charset.Charset;
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) secured object. This class is thread-safe.
014 *
015 * @author Vladimir Dzhuvinov
016 * @version 2015-05-26
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 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 signing input for this JWS object.
055         *
056         * <p>Format:
057         *
058         * <pre>
059         * [header-base64url].[payload-base64url]
060         * </pre>
061         */
062        private final String signingInputString;
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                signingInputString = composeSigningInput(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                signingInputString = composeSigningInput(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 JWSHeader getHeader() {
162
163                return header;
164        }
165
166
167        /**
168         * Composes the signing input for the specified JWS object parts.
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. 
179         *                   Must not be {@code null}.
180         *
181         * @return The signing input string.
182         */
183        private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) {
184
185                return firstPart.toString() + '.' + secondPart.toString();
186        }
187
188
189        /**
190         * Returns the signing input for this JWS object.
191         *
192         * <p>Format:
193         *
194         * <pre>
195         * [header-base64url].[payload-base64url]
196         * </pre>
197         *
198         * @return The signing input, to be passed to a JWS signer or verifier.
199         */
200        public byte[] getSigningInput() {
201
202                return signingInputString.getBytes(Charset.forName("UTF-8"));
203        }
204
205
206        /**
207         * Returns the signature of this JWS object.
208         *
209         * @return The signature, {@code null} if the JWS object is not signed 
210         *         yet.
211         */
212        public Base64URL getSignature() {
213
214                return signature;
215        }
216
217
218        /**
219         * Returns the state of this JWS object.
220         *
221         * @return The state.
222         */
223        public State getState() {
224
225                return state;
226        }
227
228
229        /**
230         * Ensures the current state is {@link State#UNSIGNED unsigned}.
231         *
232         * @throws IllegalStateException If the current state is not unsigned.
233         */
234        private void ensureUnsignedState() {
235
236                if (state != State.UNSIGNED) {
237
238                        throw new IllegalStateException("The JWS object must be in an unsigned state");
239                }
240        }
241
242
243        /**
244         * Ensures the current state is {@link State#SIGNED signed} or
245         * {@link State#VERIFIED verified}.
246         *
247         * @throws IllegalStateException If the current state is not signed or
248         *                               verified.
249         */
250        private void ensureSignedOrVerifiedState() {
251
252                if (state != State.SIGNED && state != State.VERIFIED) {
253
254                        throw new IllegalStateException("The JWS object must be in a signed or verified state");
255                }
256        }
257
258
259        /**
260         * Ensures the specified JWS signer supports the algorithm of this JWS
261         * object.
262         *
263         * @throws JOSEException If the JWS algorithm is not supported.
264         */
265        private void ensureJWSSignerSupport(final JWSSigner signer)
266                throws JOSEException {
267
268                if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) {
269
270                        throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
271                                                "\" algorithm is not allowed or supported by the JWS signer");
272                }
273        }
274
275
276        /**
277         * Signs this JWS object with the specified signer. The JWS object must
278         * be in a {@link State#UNSIGNED unsigned} state.
279         *
280         * @param signer The JWS signer. Must not be {@code null}.
281         *
282         * @throws IllegalStateException If the JWS object is not in an 
283         *                               {@link State#UNSIGNED unsigned state}.
284         * @throws JOSEException         If the JWS object couldn't be signed.
285         */
286        public synchronized void sign(final JWSSigner signer)
287                throws JOSEException {
288
289                ensureUnsignedState();
290
291                ensureJWSSignerSupport(signer);
292
293                try {
294                        signature = signer.sign(getHeader(), getSigningInput());
295
296                } catch (JOSEException e) {
297
298                        throw e;
299                                
300                } catch (Exception e) {
301
302                        // Prevent throwing unchecked exceptions at this point,
303                        // see issue #20
304                        throw new JOSEException(e.getMessage(), e);
305                }
306
307                state = State.SIGNED;
308        }
309
310
311        /**
312         * Checks the signature of this JWS object with the specified verifier. 
313         * The JWS object must be in a {@link State#SIGNED signed} state.
314         *
315         * @param verifier The JWS verifier. Must not be {@code null}.
316         *
317         * @return {@code true} if the signature was successfully verified, 
318         *         else {@code false}.
319         *
320         * @throws IllegalStateException If the JWS object is not in a 
321         *                               {@link State#SIGNED signed} or
322         *                               {@link State#VERIFIED verified state}.
323         * @throws JOSEException         If the JWS object couldn't be
324         *                               verified.
325         */
326        public synchronized boolean verify(final JWSVerifier verifier)
327                throws JOSEException {
328
329                ensureSignedOrVerifiedState();
330
331                boolean verified;
332
333                try {
334                        verified = verifier.verify(getHeader(), getSigningInput(), getSignature());
335
336                } catch (JOSEException e) {
337
338                        throw e;
339
340                } catch (Exception e) {
341
342                        // Prevent throwing unchecked exceptions at this point,
343                        // see issue #20
344                        throw new JOSEException(e.getMessage(), e);
345                }
346
347                if (verified) {
348
349                        state = State.VERIFIED;
350                }
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 
359         * must be in a {@link State#SIGNED signed} or 
360         * {@link State#VERIFIED verified} 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                return signingInputString + '.' + signature.toString();
378        }
379
380
381        /**
382         * Parses a JWS object from the specified string in compact format. The
383         * parsed JWS object will be given a {@link State#SIGNED} state.
384         *
385         * @param s The string to parse. Must not be {@code null}.
386         *
387         * @return The JWS object.
388         *
389         * @throws ParseException If the string couldn't be parsed to a valid 
390         *                        JWS object.
391         */
392        public static JWSObject parse(final String s)
393                throws ParseException {
394
395                Base64URL[] parts = JOSEObject.split(s);
396
397                if (parts.length != 3) {
398
399                        throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
400                }
401
402                return new JWSObject(parts[0], parts[1], parts[2]);
403        }
404}