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