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