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