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 2016-04-13
017 */
018@ThreadSafe
019public class JWSObject extends JOSEObject {
020
021
022        private static final long serialVersionUID = 1L;
023
024
025        /**
026         * Enumeration of the states of a JSON Web Signature (JWS) object.
027         */
028        public 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 signing input for this JWS object.
058         *
059         * <p>Format:
060         *
061         * <pre>
062         * [header-base64url].[payload-base64url]
063         * </pre>
064         */
065        private final String signingInputString;
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
092                        throw new IllegalArgumentException("The JWS header must not be null");
093                }
094
095                this.header = header;
096
097                if (payload == null) {
098
099                        throw new IllegalArgumentException("The payload must not be null");
100                }
101
102                setPayload(payload);
103
104                signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL());
105
106                signature = null;
107
108                state = State.UNSIGNED;
109        }
110
111
112        /**
113         * Creates a new signed JSON Web Signature (JWS) object with the 
114         * specified serialised parts. The state will be 
115         * {@link State#SIGNED signed}.
116         *
117         * @param firstPart  The first part, corresponding to the JWS header. 
118         *                   Must not be {@code null}.
119         * @param secondPart The second part, corresponding to the payload. Must
120         *                   not be {@code null}.
121         * @param thirdPart  The third part, corresponding to the signature.
122         *                   Must not be {@code null}.
123         *
124         * @throws ParseException If parsing of the serialised parts failed.
125         */
126        public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart)      
127                throws ParseException {
128
129                if (firstPart == null) {
130
131                        throw new IllegalArgumentException("The first part must not be null");
132                }
133
134                try {
135                        this.header = JWSHeader.parse(firstPart);
136
137                } catch (ParseException e) {
138
139                        throw new ParseException("Invalid JWS header: " + e.getMessage(), 0);
140                }
141
142                if (secondPart == null) {
143
144                        throw new IllegalArgumentException("The second part must not be null");
145                }
146
147                setPayload(new Payload(secondPart));
148
149                signingInputString = composeSigningInput(firstPart, secondPart);
150
151                if (thirdPart == null) {
152                        throw new IllegalArgumentException("The third part must not be null");
153                }
154
155                signature = thirdPart;
156
157                state = State.SIGNED; // but signature not verified yet!
158
159                setParsedParts(firstPart, secondPart, thirdPart);
160        }
161
162
163        @Override
164        public JWSHeader getHeader() {
165
166                return header;
167        }
168
169
170        /**
171         * Composes the signing input for the specified JWS object parts.
172         *
173         * <p>Format:
174         *
175         * <pre>
176         * [header-base64url].[payload-base64url]
177         * </pre>
178         *
179         * @param firstPart  The first part, corresponding to the JWS header.
180         *                   Must not be {@code null}.
181         * @param secondPart The second part, corresponding to the payload. 
182         *                   Must not be {@code null}.
183         *
184         * @return The signing input string.
185         */
186        private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) {
187
188                return firstPart.toString() + '.' + secondPart.toString();
189        }
190
191
192        /**
193         * Returns the signing input for this JWS object.
194         *
195         * <p>Format:
196         *
197         * <pre>
198         * [header-base64url].[payload-base64url]
199         * </pre>
200         *
201         * @return The signing input, to be passed to a JWS signer or verifier.
202         */
203        public byte[] getSigningInput() {
204
205                return signingInputString.getBytes(Charset.forName("UTF-8"));
206        }
207
208
209        /**
210         * Returns 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         * Returns 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
241                        throw new IllegalStateException("The JWS object must be in an unsigned state");
242                }
243        }
244
245
246        /**
247         * Ensures the current state is {@link State#SIGNED signed} or
248         * {@link State#VERIFIED verified}.
249         *
250         * @throws IllegalStateException If the current state is not signed or
251         *                               verified.
252         */
253        private void ensureSignedOrVerifiedState() {
254
255                if (state != State.SIGNED && state != State.VERIFIED) {
256
257                        throw new IllegalStateException("The JWS object must be in a signed or verified state");
258                }
259        }
260
261
262        /**
263         * Ensures the specified JWS signer supports the algorithm of this JWS
264         * object.
265         *
266         * @throws JOSEException If the JWS algorithm is not supported.
267         */
268        private void ensureJWSSignerSupport(final JWSSigner signer)
269                throws JOSEException {
270
271                if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) {
272
273                        throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
274                                                "\" algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms());
275                }
276        }
277
278
279        /**
280         * Signs this JWS object with the specified signer. The JWS object must
281         * be in a {@link State#UNSIGNED unsigned} state.
282         *
283         * @param signer The JWS signer. Must not be {@code null}.
284         *
285         * @throws IllegalStateException If the JWS object is not in an 
286         *                               {@link State#UNSIGNED unsigned state}.
287         * @throws JOSEException         If the JWS object couldn't be signed.
288         */
289        public synchronized void sign(final JWSSigner signer)
290                throws JOSEException {
291
292                ensureUnsignedState();
293
294                ensureJWSSignerSupport(signer);
295
296                try {
297                        signature = signer.sign(getHeader(), getSigningInput());
298
299                } catch (JOSEException e) {
300
301                        throw e;
302                                
303                } catch (Exception e) {
304
305                        // Prevent throwing unchecked exceptions at this point,
306                        // see issue #20
307                        throw new JOSEException(e.getMessage(), e);
308                }
309
310                state = State.SIGNED;
311        }
312
313
314        /**
315         * Checks the signature of this JWS object with the specified verifier. 
316         * The JWS object must be in a {@link State#SIGNED signed} state.
317         *
318         * @param verifier The JWS verifier. Must not be {@code null}.
319         *
320         * @return {@code true} if the signature was successfully verified, 
321         *         else {@code false}.
322         *
323         * @throws IllegalStateException If the JWS object is not in a 
324         *                               {@link State#SIGNED signed} or
325         *                               {@link State#VERIFIED verified state}.
326         * @throws JOSEException         If the JWS object couldn't be
327         *                               verified.
328         */
329        public synchronized boolean verify(final JWSVerifier verifier)
330                throws JOSEException {
331
332                ensureSignedOrVerifiedState();
333
334                boolean verified;
335
336                try {
337                        verified = verifier.verify(getHeader(), getSigningInput(), getSignature());
338
339                } catch (JOSEException e) {
340
341                        throw e;
342
343                } catch (Exception e) {
344
345                        // Prevent throwing unchecked exceptions at this point,
346                        // see issue #20
347                        throw new JOSEException(e.getMessage(), e);
348                }
349
350                if (verified) {
351
352                        state = State.VERIFIED;
353                }
354
355                return verified;
356        }
357
358
359        /**
360         * Serialises this JWS object to its compact format consisting of 
361         * Base64URL-encoded parts delimited by period ('.') characters. It 
362         * must be in a {@link State#SIGNED signed} or 
363         * {@link State#VERIFIED verified} state.
364         *
365         * <pre>
366         * [header-base64url].[payload-base64url].[signature-base64url]
367         * </pre>
368         *
369         * @return The serialised JWS object.
370         *
371         * @throws IllegalStateException If the JWS object is not in a 
372         *                               {@link State#SIGNED signed} or
373         *                               {@link State#VERIFIED verified} state.
374         */
375        @Override
376        public String serialize() {
377
378                ensureSignedOrVerifiedState();
379
380                return signingInputString + '.' + signature.toString();
381        }
382
383
384        /**
385         * Parses a JWS object from the specified string in compact format. The
386         * parsed JWS object will be given a {@link State#SIGNED} state.
387         *
388         * @param s The string to parse. Must not be {@code null}.
389         *
390         * @return The JWS object.
391         *
392         * @throws ParseException If the string couldn't be parsed to a valid 
393         *                        JWS object.
394         */
395        public static JWSObject parse(final String s)
396                throws ParseException {
397
398                Base64URL[] parts = JOSEObject.split(s);
399
400                if (parts.length != 3) {
401
402                        throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
403                }
404
405                return new JWSObject(parts[0], parts[1], parts[2]);
406        }
407}