001package com.nimbusds.jose;
002
003
004import java.text.ParseException;
005
006import com.nimbusds.jose.util.Base64URL;
007import com.nimbusds.jose.util.StandardCharset;
008import net.jcip.annotations.ThreadSafe;
009
010
011/**
012 * JSON Web Signature (JWS) secured object. This class is thread-safe.
013 *
014 * @author Vladimir Dzhuvinov
015 * @version 2016-07-26
016 */
017@ThreadSafe
018public class JWSObject extends JOSEObject {
019
020
021        private static final long serialVersionUID = 1L;
022
023
024        /**
025         * Enumeration of the states of a JSON Web Signature (JWS) object.
026         */
027        public enum State {
028
029
030                /**
031                 * The JWS object is created but not signed yet.
032                 */
033                UNSIGNED,
034
035
036                /**
037                 * The JWS object is signed but its signature is not verified.
038                 */
039                SIGNED,
040
041
042                /**
043                 * The JWS object is signed and its signature was successfully verified.
044                 */
045                VERIFIED
046        }
047
048
049        /**
050         * The header.
051         */
052        private final JWSHeader header;
053
054
055        /**
056         * The signing input for this JWS object.
057         *
058         * <p>Format:
059         *
060         * <pre>
061         * [header-base64url].[payload-base64url]
062         * </pre>
063         */
064        private final String signingInputString;
065
066
067        /**
068         * The signature, {@code null} if not signed.
069         */
070        private Base64URL signature;
071
072
073        /**
074         * The JWS object state.
075         */
076        private State state;
077
078
079        /**
080         * Creates a new to-be-signed JSON Web Signature (JWS) object with the 
081         * specified header and payload. The initial state will be 
082         * {@link State#UNSIGNED unsigned}.
083         *
084         * @param header  The JWS header. Must not be {@code null}.
085         * @param payload The payload. Must not be {@code null}.
086         */
087        public JWSObject(final JWSHeader header, final Payload payload) {
088
089                if (header == null) {
090
091                        throw new IllegalArgumentException("The JWS header must not be null");
092                }
093
094                this.header = header;
095
096                if (payload == null) {
097
098                        throw new IllegalArgumentException("The payload must not be null");
099                }
100
101                setPayload(payload);
102
103                signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL());
104
105                signature = null;
106
107                state = State.UNSIGNED;
108        }
109
110
111        /**
112         * Creates a new signed JSON Web Signature (JWS) object with the 
113         * specified serialised parts. The state will be 
114         * {@link State#SIGNED signed}.
115         *
116         * @param firstPart  The first part, corresponding to the JWS header. 
117         *                   Must not be {@code null}.
118         * @param secondPart The second part, corresponding to the payload. Must
119         *                   not be {@code null}.
120         * @param thirdPart  The third part, corresponding to the signature.
121         *                   Must not be {@code null}.
122         *
123         * @throws ParseException If parsing of the serialised parts failed.
124         */
125        public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart)      
126                throws ParseException {
127
128                if (firstPart == null) {
129
130                        throw new IllegalArgumentException("The first part must not be null");
131                }
132
133                try {
134                        this.header = JWSHeader.parse(firstPart);
135
136                } catch (ParseException e) {
137
138                        throw new ParseException("Invalid JWS header: " + e.getMessage(), 0);
139                }
140
141                if (secondPart == null) {
142
143                        throw new IllegalArgumentException("The second part must not be null");
144                }
145
146                setPayload(new Payload(secondPart));
147
148                signingInputString = composeSigningInput(firstPart, secondPart);
149
150                if (thirdPart == null) {
151                        throw new IllegalArgumentException("The third part must not be null");
152                }
153
154                signature = thirdPart;
155
156                state = State.SIGNED; // but signature not verified yet!
157
158                setParsedParts(firstPart, secondPart, thirdPart);
159        }
160
161
162        @Override
163        public JWSHeader getHeader() {
164
165                return header;
166        }
167
168
169        /**
170         * Composes the signing input for the specified JWS object parts.
171         *
172         * <p>Format:
173         *
174         * <pre>
175         * [header-base64url].[payload-base64url]
176         * </pre>
177         *
178         * @param firstPart  The first part, corresponding to the JWS header.
179         *                   Must not be {@code null}.
180         * @param secondPart The second part, corresponding to the payload. 
181         *                   Must not be {@code null}.
182         *
183         * @return The signing input string.
184         */
185        private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) {
186
187                return firstPart.toString() + '.' + secondPart.toString();
188        }
189
190
191        /**
192         * Returns the signing input for this JWS object.
193         *
194         * <p>Format:
195         *
196         * <pre>
197         * [header-base64url].[payload-base64url]
198         * </pre>
199         *
200         * @return The signing input, to be passed to a JWS signer or verifier.
201         */
202        public byte[] getSigningInput() {
203
204                return signingInputString.getBytes(StandardCharset.UTF_8);
205        }
206
207
208        /**
209         * Returns the signature of this JWS object.
210         *
211         * @return The signature, {@code null} if the JWS object is not signed 
212         *         yet.
213         */
214        public Base64URL getSignature() {
215
216                return signature;
217        }
218
219
220        /**
221         * Returns the state of this JWS object.
222         *
223         * @return The state.
224         */
225        public State getState() {
226
227                return state;
228        }
229
230
231        /**
232         * Ensures the current state is {@link State#UNSIGNED unsigned}.
233         *
234         * @throws IllegalStateException If the current state is not unsigned.
235         */
236        private void ensureUnsignedState() {
237
238                if (state != State.UNSIGNED) {
239
240                        throw new IllegalStateException("The JWS object must be in an unsigned state");
241                }
242        }
243
244
245        /**
246         * Ensures the current state is {@link State#SIGNED signed} or
247         * {@link State#VERIFIED verified}.
248         *
249         * @throws IllegalStateException If the current state is not signed or
250         *                               verified.
251         */
252        private void ensureSignedOrVerifiedState() {
253
254                if (state != State.SIGNED && state != State.VERIFIED) {
255
256                        throw new IllegalStateException("The JWS object must be in a signed or verified state");
257                }
258        }
259
260
261        /**
262         * Ensures the specified JWS signer supports the algorithm of this JWS
263         * object.
264         *
265         * @throws JOSEException If the JWS algorithm is not supported.
266         */
267        private void ensureJWSSignerSupport(final JWSSigner signer)
268                throws JOSEException {
269
270                if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) {
271
272                        throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
273                                                "\" algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms());
274                }
275        }
276
277
278        /**
279         * Signs this JWS object with the specified signer. The JWS object must
280         * be in a {@link State#UNSIGNED unsigned} state.
281         *
282         * @param signer The JWS signer. Must not be {@code null}.
283         *
284         * @throws IllegalStateException If the JWS object is not in an 
285         *                               {@link State#UNSIGNED unsigned state}.
286         * @throws JOSEException         If the JWS object couldn't be signed.
287         */
288        public synchronized void sign(final JWSSigner signer)
289                throws JOSEException {
290
291                ensureUnsignedState();
292
293                ensureJWSSignerSupport(signer);
294
295                try {
296                        signature = signer.sign(getHeader(), getSigningInput());
297
298                } catch (JOSEException e) {
299
300                        throw e;
301                                
302                } catch (Exception e) {
303
304                        // Prevent throwing unchecked exceptions at this point,
305                        // see issue #20
306                        throw new JOSEException(e.getMessage(), e);
307                }
308
309                state = State.SIGNED;
310        }
311
312
313        /**
314         * Checks the signature of this JWS object with the specified verifier. 
315         * The JWS object must be in a {@link State#SIGNED signed} state.
316         *
317         * @param verifier The JWS verifier. Must not be {@code null}.
318         *
319         * @return {@code true} if the signature was successfully verified, 
320         *         else {@code false}.
321         *
322         * @throws IllegalStateException If the JWS object is not in a 
323         *                               {@link State#SIGNED signed} or
324         *                               {@link State#VERIFIED verified state}.
325         * @throws JOSEException         If the JWS object couldn't be
326         *                               verified.
327         */
328        public synchronized boolean verify(final JWSVerifier verifier)
329                throws JOSEException {
330
331                ensureSignedOrVerifiedState();
332
333                boolean verified;
334
335                try {
336                        verified = verifier.verify(getHeader(), getSigningInput(), getSignature());
337
338                } catch (JOSEException e) {
339
340                        throw e;
341
342                } catch (Exception e) {
343
344                        // Prevent throwing unchecked exceptions at this point,
345                        // see issue #20
346                        throw new JOSEException(e.getMessage(), e);
347                }
348
349                if (verified) {
350
351                        state = State.VERIFIED;
352                }
353
354                return verified;
355        }
356
357
358        /**
359         * Serialises this JWS object to its compact format consisting of 
360         * Base64URL-encoded parts delimited by period ('.') characters. It 
361         * must be in a {@link State#SIGNED signed} or 
362         * {@link State#VERIFIED verified} state.
363         *
364         * <pre>
365         * [header-base64url].[payload-base64url].[signature-base64url]
366         * </pre>
367         *
368         * @return The serialised JWS object.
369         *
370         * @throws IllegalStateException If the JWS object is not in a 
371         *                               {@link State#SIGNED signed} or
372         *                               {@link State#VERIFIED verified} state.
373         */
374        @Override
375        public String serialize() {
376
377                ensureSignedOrVerifiedState();
378
379                return signingInputString + '.' + signature.toString();
380        }
381
382
383        /**
384         * Parses a JWS object from the specified string in compact format. The
385         * parsed JWS object will be given a {@link State#SIGNED} state.
386         *
387         * @param s The string to parse. Must not be {@code null}.
388         *
389         * @return The JWS object.
390         *
391         * @throws ParseException If the string couldn't be parsed to a valid 
392         *                        JWS object.
393         */
394        public static JWSObject parse(final String s)
395                throws ParseException {
396
397                Base64URL[] parts = JOSEObject.split(s);
398
399                if (parts.length != 3) {
400
401                        throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
402                }
403
404                return new JWSObject(parts[0], parts[1], parts[2]);
405        }
406}