001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2023, Connect2id Ltd and contributors.
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.JSONArrayUtils;
023import com.nimbusds.jose.util.JSONObjectUtils;
024import net.jcip.annotations.Immutable;
025import net.jcip.annotations.ThreadSafe;
026
027import java.text.ParseException;
028import java.util.*;
029import java.util.concurrent.atomic.AtomicBoolean;
030
031
032/**
033 * JSON Web Signature (JWS) secured object with
034 * <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.2">JSON
035 * serialisation</a>.
036 *
037 * <p>This class is thread-safe.
038 *
039 * @author Alexander Martynov
040 * @author Vladimir Dzhuvinov
041 * @version 2024-04-20
042 */
043@ThreadSafe
044public class JWSObjectJSON extends JOSEObjectJSON {
045        
046        
047        private static final long serialVersionUID = 1L;
048        
049        
050        /**
051         * Individual signature in a JWS secured object serialisable to JSON.
052         */
053        @Immutable
054        public static final class Signature {
055                
056                
057                /**
058                 * The payload.
059                 */
060                private final Payload payload;
061                
062                
063                /**
064                 * The JWS protected header, {@code null} if none.
065                 */
066                private final JWSHeader header;
067                
068                
069                /**
070                 * The unprotected header, {@code null} if none.
071                 */
072                private final UnprotectedHeader unprotectedHeader;
073                
074                
075                /**
076                 * The signature.
077                 */
078                private final Base64URL signature;
079                
080                
081                /**
082                 * The signature verified state.
083                 */
084                private final AtomicBoolean verified = new AtomicBoolean(false);
085                
086                
087                /**
088                 * Creates a new parsed signature.
089                 *
090                 * @param payload           The payload. Must not be
091                 *                          {@code null}.
092                 * @param header            The JWS protected header,
093                 *                          {@code null} if none.
094                 * @param unprotectedHeader The unprotected header,
095                 *                          {@code null} if none.
096                 * @param signature         The signature. Must not be
097                 *                          {@code null}.
098                 */
099                private Signature(final Payload payload,
100                                  final JWSHeader header,
101                                  final UnprotectedHeader unprotectedHeader,
102                                  final Base64URL signature) {
103                        
104                        Objects.requireNonNull(payload);
105                        this.payload = payload;
106                        
107                        this.header = header;
108                        this.unprotectedHeader = unprotectedHeader;
109                        
110                        Objects.requireNonNull(signature);
111                        this.signature = signature;
112                }
113                
114                
115                /**
116                 * Returns the JWS protected header.
117                 *
118                 * @return The JWS protected header, {@code null} if none.
119                 */
120                public JWSHeader getHeader() {
121                        return header;
122                }
123                
124                
125                /**
126                 * Returns the unprotected header.
127                 *
128                 * @return The unprotected header, {@code null} if none.
129                 */
130                public UnprotectedHeader getUnprotectedHeader() {
131                        return unprotectedHeader;
132                }
133                
134                
135                /**
136                 * Returns the signature.
137                 *
138                 * @return The signature.
139                 */
140                public Base64URL getSignature() {
141                        return signature;
142                }
143                
144                
145                /**
146                 * Returns a JSON object representation for use in the general
147                 * and flattened serialisations.
148                 *
149                 * @return The JSON object.
150                 */
151                private Map<String, Object> toJSONObject() {
152                        
153                        Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
154                        
155                        if (header != null) {
156                                jsonObject.put("protected", header.toBase64URL().toString());
157                        }
158                        
159                        if (unprotectedHeader != null && ! unprotectedHeader.getIncludedParams().isEmpty()) {
160                                jsonObject.put("header", unprotectedHeader.toJSONObject());
161                        }
162                        
163                        jsonObject.put("signature", signature.toString());
164                        
165                        return jsonObject;
166                }
167                
168                
169                /**
170                 * Returns the compact JWS object representation of this
171                 * individual signature.
172                 *
173                 * @return The JWS object serialisable to compact encoding.
174                 */
175                public JWSObject toJWSObject() {
176                        
177                        try {
178                                return new JWSObject(header.toBase64URL(), payload.toBase64URL(), signature);
179                        } catch (ParseException e) {
180                                throw new IllegalStateException();
181                        }
182                }
183                
184                
185                /**
186                 * Returns {@code true} if the signature was successfully
187                 * verified with a previous call to {@link #verify}.
188                 *
189                 * @return {@code true} if the signature was successfully
190                 *         verified, {@code false} if the signature is invalid
191                 *         or {@link #verify} was never called.
192                 */
193                public boolean isVerified() {
194                        return verified.get();
195                }
196                
197                
198                /**
199                 * Checks the signature with the specified verifier.
200                 *
201                 * @param verifier The JWS verifier. Must not be {@code null}.
202                 *
203                 * @return {@code true} if the signature was successfully
204                 *         verified, else {@code false}.
205                 *
206                 * @throws JOSEException If the signature verification failed.
207                 */
208                public synchronized boolean verify(final JWSVerifier verifier)
209                        throws JOSEException {
210                        
211                        try {
212                                verified.set(toJWSObject().verify(verifier));
213                        } catch (JOSEException e) {
214                                throw e;
215                        } catch (Exception e) {
216                                // Prevent throwing unchecked exceptions at this point,
217                                // see issue #20
218                                throw new JOSEException(e.getMessage(), e);
219                        }
220                        
221                        return verified.get();
222                }
223        }
224        
225        
226        /**
227         * Enumeration of the states of a JSON Web Signature (JWS) secured
228         * object serialisable to JSON.
229         */
230        public enum State {
231                
232                
233                /**
234                 * The object is not signed yet.
235                 */
236                UNSIGNED,
237                
238                
239                /**
240                 * The object has one or more signatures; they are not (all)
241                 * verified.
242                 */
243                SIGNED,
244                
245                
246                /**
247                 * All signatures are verified.
248                 */
249                VERIFIED
250        }
251        
252        
253        /**
254         * The applied signatures.
255         */
256        private final List<Signature> signatures = new LinkedList<>();
257        
258        
259        /**
260         * Creates a new to-be-signed JSON Web Signature (JWS) secured object
261         * with the specified payload.
262         *
263         * @param payload The payload. Must not be {@code null}.
264         */
265        public JWSObjectJSON(final Payload payload) {
266                
267                super(payload);
268                Objects.requireNonNull(payload, "The payload must not be null");
269        }
270        
271        
272        /**
273         * Creates a new JSON Web Signature (JWS) secured object with one or
274         * more signatures.
275         *
276         * @param payload    The payload. Must not be {@code null}.
277         * @param signatures The signatures. Must be at least one.
278         */
279        private JWSObjectJSON(final Payload payload,
280                              final List<Signature> signatures) {
281                
282                super(Objects.requireNonNull(payload, "The payload must not be null"));
283                
284                if (signatures.isEmpty()) {
285                        throw new IllegalArgumentException("At least one signature required");
286                }
287                
288                this.signatures.addAll(signatures);
289        }
290        
291        
292        /**
293         * Returns the individual signatures.
294         *
295         * @return The individual signatures, as an unmodified list, empty list
296         *         if none have been added.
297         */
298        public List<Signature> getSignatures() {
299                
300                return Collections.unmodifiableList(signatures);
301        }
302        
303        
304        /**
305         * Signs this JWS secured object with the specified JWS signer and
306         * adds the resulting signature to it. To add multiple
307         * {@link #getSignatures() signatures} call this method successively.
308         *
309         * @param jwsHeader The JWS protected header. The algorithm specified
310         *                  by the header must be supported by the JWS signer.
311         *                  Must not be {@code null}.
312         * @param signer    The JWS signer. Must not be {@code null}.
313         *
314         * @throws JOSEException If the JWS object couldn't be signed.
315         */
316        public synchronized void sign(final JWSHeader jwsHeader,
317                                      final JWSSigner signer)
318                throws JOSEException {
319                
320                sign(jwsHeader, null, signer);
321        }
322        
323        
324        /**
325         * Signs this JWS secured object with the specified JWS signer and
326         * adds the resulting signature to it. To add multiple
327         * {@link #getSignatures() signatures} call this method successively.
328         *
329         * @param jwsHeader              The JWS protected header. The
330         *                               algorithm specified by the header must
331         *                               be supported by the JWS signer. Must
332         *                               not be {@code null}.
333         * @param unprotectedHeader      The unprotected header to include,
334         *                               {@code null} if none.
335         * @param signer                 The JWS signer. Must not be
336         *                               {@code null}.
337         *
338         * @throws JOSEException If the JWS object couldn't be signed.
339         */
340        public synchronized void sign(final JWSHeader jwsHeader,
341                                      final UnprotectedHeader unprotectedHeader,
342                                      final JWSSigner signer)
343                throws JOSEException {
344                
345                try {
346                        HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
347                } catch (IllegalHeaderException e) {
348                        throw new IllegalArgumentException(e.getMessage(), e);
349                }
350                
351                JWSObject jwsObject = new JWSObject(jwsHeader, getPayload());
352                jwsObject.sign(signer);
353                
354                signatures.add(new Signature(getPayload(), jwsHeader, unprotectedHeader, jwsObject.getSignature()));
355        }
356        
357        
358        /**
359         * Returns the current signatures state.
360         *
361         * @return The state.
362         */
363        public State getState() {
364                
365                if (getSignatures().isEmpty()) {
366                        return State.UNSIGNED;
367                }
368                
369                for (Signature sig: getSignatures()) {
370                        if (! sig.isVerified()) {
371                                return State.SIGNED;
372                        }
373                }
374                
375                return State.VERIFIED;
376        }
377        
378        
379        @Override
380        public Map<String, Object> toGeneralJSONObject() {
381                
382                if (signatures.size() < 1) {
383                        throw new IllegalStateException("The general JWS JSON serialization requires at least one signature");
384                }
385                
386                Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
387                jsonObject.put("payload", getPayload().toBase64URL().toString());
388                
389                List<Object> signaturesJSONArray = JSONArrayUtils.newJSONArray();
390                
391                for (Signature signature: getSignatures()) {
392                        Map<String, Object> signatureJSONObject = signature.toJSONObject();
393                        signaturesJSONArray.add(signatureJSONObject);
394                }
395                
396                jsonObject.put("signatures", signaturesJSONArray);
397                
398                return jsonObject;
399        }
400        
401        
402        @Override
403        public Map<String, Object> toFlattenedJSONObject() {
404                
405                if (signatures.size() != 1) {
406                        throw new IllegalStateException("The flattened JWS JSON serialization requires exactly one signature");
407                }
408                
409                Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
410                jsonObject.put("payload", getPayload().toBase64URL().toString());
411                jsonObject.putAll(getSignatures().get(0).toJSONObject());
412                return jsonObject;
413        }
414        
415        
416        @Override
417        public String serializeGeneral() {
418                return JSONObjectUtils.toJSONString(toGeneralJSONObject());
419        }
420        
421        
422        @Override
423        public String serializeFlattened() {
424                return JSONObjectUtils.toJSONString(toFlattenedJSONObject());
425        }
426        
427        
428        private static JWSHeader parseJWSHeader(final Map<String, Object> jsonObject)
429                throws ParseException {
430                
431                Base64URL protectedHeader = JSONObjectUtils.getBase64URL(jsonObject, "protected");
432                
433                if (protectedHeader == null) {
434                        throw new ParseException("Missing protected header (required by this library)", 0);
435                }
436                
437                try {
438                        return JWSHeader.parse(protectedHeader);
439                } catch (ParseException e) {
440                        if ("Not a JWS header".equals(e.getMessage())) {
441                                // alg required by this library (not the spec)
442                                throw new ParseException("Missing JWS \"alg\" parameter in protected header (required by this library)", 0);
443                        }
444                        throw e;
445                }
446        }
447        
448        
449        /**
450         * Parses a JWS secured object from the specified JSON object
451         * representation.
452         *
453         * @param jsonObject The JSON object to parse. Must not be
454         *                   {@code null}.
455         *
456         * @return The JWS secured object.
457         *
458         * @throws ParseException If the JSON object couldn't be parsed to a
459         *                        JWS secured object.
460         */
461        public static JWSObjectJSON parse(final Map<String, Object> jsonObject)
462                throws ParseException {
463                
464                // Payload always present
465                Base64URL payloadB64URL = JSONObjectUtils.getBase64URL(jsonObject, "payload");
466                
467                if (payloadB64URL == null) {
468                        throw new ParseException("Missing payload", 0);
469                }
470                
471                Payload payload = new Payload(payloadB64URL);
472                
473                // Signature present at top-level in flattened JSON
474                Base64URL topLevelSignatureB64 = JSONObjectUtils.getBase64URL(jsonObject, "signature");
475                
476                boolean flattened = topLevelSignatureB64 != null;
477                
478                List<Signature> signatureList = new LinkedList<>();
479                
480                if (flattened) {
481                        
482                        JWSHeader jwsHeader = parseJWSHeader(jsonObject);
483                        
484                        UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header"));
485                        
486                        // https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.2
487                        // "The "signatures" member MUST NOT be present when using this syntax."
488                        if (jsonObject.get("signatures") != null) {
489                                throw new ParseException("The \"signatures\" member must not be present in flattened JWS JSON serialization", 0);
490                        }
491                        
492                        try {
493                                HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
494                        } catch (IllegalHeaderException e) {
495                                throw new ParseException(e.getMessage(), 0);
496                        }
497                        
498                        signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, topLevelSignatureB64));
499                        
500                } else {
501                        Map<String, Object>[] signatures = JSONObjectUtils.getJSONObjectArray(jsonObject, "signatures");
502                        if (signatures == null || signatures.length == 0) {
503                                throw new ParseException("The \"signatures\" member must be present in general JSON Serialization", 0);
504                        }
505                        
506                        for (Map<String, Object> signatureJSONObject: signatures) {
507                                
508                                JWSHeader jwsHeader = parseJWSHeader(signatureJSONObject);
509                                
510                                UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(signatureJSONObject, "header"));
511                                
512                                try {
513                                        HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
514                                } catch (IllegalHeaderException e) {
515                                        throw new ParseException(e.getMessage(), 0);
516                                }
517                                
518                                Base64URL signatureB64 = JSONObjectUtils.getBase64URL(signatureJSONObject, "signature");
519                                
520                                if (signatureB64 == null) {
521                                        throw new ParseException("Missing \"signature\" member", 0);
522                                }
523                                
524                                signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, signatureB64));
525                        }
526                }
527                
528                return new JWSObjectJSON(payload, signatureList);
529        }
530        
531        
532        /**
533         * Parses a JWS secured object from the specified JSON object string.
534         *
535         * @param json The JSON object string to parse. Must not be
536         *             {@code null}.
537         *
538         * @return The JWS secured object.
539         *
540         * @throws ParseException If the string couldn't be parsed to a JWS
541         *                        secured object.
542         */
543        public static JWSObjectJSON parse(final String json)
544                throws ParseException {
545                
546                return parse(JSONObjectUtils.parse(json));
547        }
548}