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 net.jcip.annotations.ThreadSafe;
024
025import com.nimbusds.jose.util.Base64URL;
026import com.nimbusds.jose.util.StandardCharset;
027
028
029/**
030 * JSON Web Signature (JWS) secured object. This class is thread-safe.
031 *
032 * @author Vladimir Dzhuvinov
033 * @version 2020-04-16
034 */
035@ThreadSafe
036public class JWSObject extends JOSEObject {
037
038
039        private static final long serialVersionUID = 1L;
040
041
042        /**
043         * Enumeration of the states of a JSON Web Signature (JWS) object.
044         */
045        public enum State {
046
047
048                /**
049                 * The JWS object is created but not signed yet.
050                 */
051                UNSIGNED,
052
053
054                /**
055                 * The JWS object is signed but its signature is not verified.
056                 */
057                SIGNED,
058
059
060                /**
061                 * The JWS object is signed and its signature was successfully verified.
062                 */
063                VERIFIED
064        }
065
066
067        /**
068         * The header.
069         */
070        private final JWSHeader header;
071
072
073        /**
074         * The signing input for this JWS object.
075         */
076        private final String signingInputString;
077
078
079        /**
080         * The signature, {@code null} if not signed.
081         */
082        private Base64URL signature;
083
084
085        /**
086         * The JWS object state.
087         */
088        private State state;
089
090
091        /**
092         * Creates a new to-be-signed JSON Web Signature (JWS) object with the 
093         * specified header and payload. The initial state will be 
094         * {@link State#UNSIGNED unsigned}.
095         *
096         * @param header  The JWS header. Must not be {@code null}.
097         * @param payload The payload. Must not be {@code null}.
098         */
099        public JWSObject(final JWSHeader header, final Payload payload) {
100
101                if (header == null) {
102                        throw new IllegalArgumentException("The JWS header must not be null");
103                }
104                this.header = header;
105
106                if (payload == null) {
107                        throw new IllegalArgumentException("The payload must not be null");
108                }
109                setPayload(payload);
110                
111                signingInputString = composeSigningInput();
112                signature = null;
113                state = State.UNSIGNED;
114        }
115
116
117        /**
118         * Creates a new signed JSON Web Signature (JWS) object with the
119         * specified serialised parts. The state will be
120         * {@link State#SIGNED signed}.
121         *
122         * @param firstPart  The first part, corresponding to the JWS header.
123         *                   Must not be {@code null}.
124         * @param secondPart The second part, corresponding to the payload.
125         *                   Must not be {@code null}.
126         * @param thirdPart  The third part, corresponding to the signature.
127         *                   Must not be {@code null}.
128         *
129         * @throws ParseException If parsing of the serialised parts failed.
130         */
131        public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart)
132                throws ParseException {
133                this(firstPart, new Payload(secondPart), thirdPart);
134        }
135
136        
137        /**
138         * Creates a new signed JSON Web Signature (JWS) object with the
139         * specified serialised parts and payload which can be optionally
140         * unencoded (RFC 7797). The state will be {@link State#SIGNED signed}.
141         *
142         * @param firstPart The first part, corresponding to the JWS header.
143         *                  Must not be {@code null}.
144         * @param payload   The payload. Must not be {@code null}.
145         * @param thirdPart The third part, corresponding to the signature.
146         *                  Must not be {@code null}.
147         *
148         * @throws ParseException If parsing of the serialised parts failed.
149         */
150        public JWSObject(final Base64URL firstPart, final Payload payload, final Base64URL thirdPart)
151                throws ParseException {
152
153                if (firstPart == null) {
154                        throw new IllegalArgumentException("The first part must not be null");
155                }
156                try {
157                        this.header = JWSHeader.parse(firstPart);
158                } catch (ParseException e) {
159                        throw new ParseException("Invalid JWS header: " + e.getMessage(), 0);
160                }
161
162                if (payload == null) {
163                        throw new IllegalArgumentException("The payload (second part) must not be null");
164                }
165                setPayload(payload);
166                
167                signingInputString = composeSigningInput();
168
169                if (thirdPart == null) {
170                        throw new IllegalArgumentException("The third part must not be null");
171                }
172                signature = thirdPart;
173                state = State.SIGNED; // but signature not verified yet!
174
175                if (getHeader().isBase64URLEncodePayload()) {
176                        setParsedParts(firstPart, payload.toBase64URL(), thirdPart);
177                } else {
178                        setParsedParts(firstPart, new Base64URL(""), thirdPart);
179                }
180        }
181
182        @Override
183        public JWSHeader getHeader() {
184
185                return header;
186        }
187
188
189        /**
190         * Composes the signing input string from the header and payload.
191         *
192         * @return The signing input string.
193         */
194        private String composeSigningInput() {
195                
196                if (header.isBase64URLEncodePayload()) {
197                        return getHeader().toBase64URL().toString() + '.' + getPayload().toBase64URL().toString();
198                } else {
199                        return getHeader().toBase64URL().toString() + '.' + getPayload().toString();
200                }
201        }
202
203
204        /**
205         * Returns the signing input for this JWS object.
206         *
207         * @return The signing input, to be passed to a JWS signer or verifier.
208         */
209        public byte[] getSigningInput() {
210                
211                return signingInputString.getBytes(StandardCharset.UTF_8);
212        }
213
214
215        /**
216         * Returns 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         * Returns 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.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) {
278
279                        throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
280                                                "\" algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms());
281                }
282        }
283
284
285        /**
286         * Signs this JWS object with the specified signer. The JWS object must
287         * be in a {@link State#UNSIGNED unsigned} state.
288         *
289         * @param signer The JWS signer. Must not be {@code null}.
290         *
291         * @throws IllegalStateException If the JWS object is not in an 
292         *                               {@link State#UNSIGNED unsigned state}.
293         * @throws JOSEException         If the JWS object couldn't be signed.
294         */
295        public synchronized void sign(final JWSSigner signer)
296                throws JOSEException {
297
298                ensureUnsignedState();
299
300                ensureJWSSignerSupport(signer);
301
302                try {
303                        signature = signer.sign(getHeader(), getSigningInput());
304
305                } catch (JOSEException e) {
306
307                        throw e;
308                                
309                } catch (Exception e) {
310
311                        // Prevent throwing unchecked exceptions at this point,
312                        // see issue #20
313                        throw new JOSEException(e.getMessage(), e);
314                }
315
316                state = State.SIGNED;
317        }
318
319
320        /**
321         * Checks the signature of this JWS object with the specified verifier.
322         * The JWS object must be in a {@link State#SIGNED signed} state.
323         *
324         * @param verifier The JWS verifier. Must not be {@code null}.
325         *
326         * @return {@code true} if the signature was successfully verified,
327         *         else {@code false}.
328         *
329         * @throws IllegalStateException If the JWS object is not in a
330         *                               {@link State#SIGNED signed} or
331         *                               {@link State#VERIFIED verified state}.
332         * @throws JOSEException         If the JWS object couldn't be
333         *                               verified.
334         */
335        public synchronized boolean verify(final JWSVerifier verifier)
336                throws JOSEException {
337
338                ensureSignedOrVerifiedState();
339
340                boolean verified;
341
342                try {
343                        verified = verifier.verify(getHeader(), getSigningInput(), getSignature());
344
345                } catch (JOSEException e) {
346
347                        throw e;
348
349                } catch (Exception e) {
350
351                        // Prevent throwing unchecked exceptions at this point,
352                        // see issue #20
353                        throw new JOSEException(e.getMessage(), e);
354                }
355
356                if (verified) {
357
358                        state = State.VERIFIED;
359                }
360
361                return verified;
362        }
363
364
365        /**
366         * Serialises this JWS object to its compact format consisting of 
367         * Base64URL-encoded parts delimited by period ('.') characters. It 
368         * must be in a {@link State#SIGNED signed} or 
369         * {@link State#VERIFIED verified} state.
370         *
371         * <pre>
372         * [header-base64url].[payload-base64url].[signature-base64url]
373         * </pre>
374         *
375         * @return The serialised JWS object.
376         *
377         * @throws IllegalStateException If the JWS object is not in a 
378         *                               {@link State#SIGNED signed} or
379         *                               {@link State#VERIFIED verified} state.
380         */
381        @Override
382        public String serialize() {
383                return serialize(false);
384        }
385
386
387        /**
388         * Serialises this JWS object to its compact format consisting of
389         * Base64URL-encoded parts delimited by period ('.') characters. It
390         * must be in a {@link State#SIGNED signed} or
391         * {@link State#VERIFIED verified} state.
392         *
393         * @param detachedPayload {@code true} to return a serialised object
394         *                        with a detached payload compliant with RFC
395         *                        7797, {@code false} for regular JWS
396         *                        serialisation.
397         *
398         * @return The serialised JOSE object.
399         *
400         * @throws IllegalStateException If the JOSE object is not in a state
401         *                               that permits serialisation.
402         */
403        public String serialize(final boolean detachedPayload) {
404                ensureSignedOrVerifiedState();
405
406                if (detachedPayload) {
407                        return header.toBase64URL().toString() + '.' + '.' + signature.toString();
408                }
409
410                return signingInputString + '.' + signature.toString();
411        }
412
413        /**
414         * Parses a JWS object from the specified string in compact format. The
415         * parsed JWS object will be given a {@link State#SIGNED} state.
416         *
417         * @param s The JWS string to parse. Must not be {@code null}.
418         *
419         * @return The JWS object.
420         *
421         * @throws ParseException If the string couldn't be parsed to a JWS
422         *                        object.
423         */
424        public static JWSObject parse(final String s)
425                throws ParseException {
426
427                Base64URL[] parts = JOSEObject.split(s);
428
429                if (parts.length != 3) {
430
431                        throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
432                }
433
434                return new JWSObject(parts[0], parts[1], parts[2]);
435        }
436        
437        
438        /**
439         * Parses a JWS object from the specified string in compact format and
440         * a detached payload which can be optionally unencoded (RFC 7797). The
441         * parsed JWS object will be given a {@link State#SIGNED} state.
442         *
443         * @param s               The JWS string to parse for a detached
444         *                        payload. Must not be {@code null}.
445         * @param detachedPayload The detached payload, optionally unencoded
446         *                        (RFC 7797). Must not be {@code null}.
447         *
448         * @return The JWS object.
449         *
450         * @throws ParseException If the string couldn't be parsed to a JWS
451         *                        object.
452         */
453        public static JWSObject parse(final String s, final Payload detachedPayload)
454                throws ParseException {
455                
456                Base64URL[] parts = JOSEObject.split(s);
457                
458                if (parts.length != 3) {
459                        throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
460                }
461                
462                if (! parts[1].toString().isEmpty()) {
463                        throw new ParseException("The payload Base64URL part must be empty", 0);
464                }
465                
466                return new JWSObject(parts[0], detachedPayload, parts[2]);
467        }
468}