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