001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2021, 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 java.text.ParseException;
022import java.util.*;
023import java.util.concurrent.atomic.AtomicBoolean;
024
025import net.jcip.annotations.Immutable;
026import net.jcip.annotations.ThreadSafe;
027
028import com.nimbusds.jose.util.Base64URL;
029import com.nimbusds.jose.util.JSONArrayUtils;
030import com.nimbusds.jose.util.JSONObjectUtils;
031
032
033/**
034 * JSON Web Signature (JWS) secured object serialisable to
035 * <a href="https://datatracker.ietf.org/doc/html/rfc7515#section-3.2">JSON</a>.
036 *
037 * <p>This class is thread-safe.
038 *
039 * @author Alexander Martynov
040 * @author Vladimir Dzhuvinov
041 * @version 2021-10-09
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, {@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(payload);
283                
284                Objects.requireNonNull(payload, "The payload must not be null");
285                
286                if (signatures.isEmpty()) {
287                        throw new IllegalArgumentException("At least one signature required");
288                }
289                
290                this.signatures.addAll(signatures);
291        }
292        
293        
294        /**
295         * Returns the individual signatures.
296         *
297         * @return The individual signatures, as an unmodified list, empty list
298         *         if none have been added.
299         */
300        public List<Signature> getSignatures() {
301                
302                return Collections.unmodifiableList(signatures);
303        }
304        
305        
306        /**
307         * Signs this JWS secured object with the specified JWS signer and
308         * adds the resulting signature to it. To add multiple
309         * {@link #getSignatures() signatures} call this method successively.
310         *
311         * @param jwsHeader The JWS protected header. The algorithm specified
312         *                  by the header must be supported by the JWS signer.
313         *                  Must not be {@code null}.
314         * @param signer    The JWS signer. Must not be {@code null}.
315         *
316         * @throws JOSEException If the JWS object couldn't be signed.
317         */
318        public synchronized void sign(final JWSHeader jwsHeader,
319                                      final JWSSigner signer)
320                throws JOSEException {
321                
322                sign(jwsHeader, null, signer);
323        }
324        
325        
326        /**
327         * Signs this JWS secured object with the specified JWS signer and
328         * adds the resulting signature to it. To add multiple
329         * {@link #getSignatures() signatures} call this method successively.
330         *
331         * @param jwsHeader              The JWS protected header. The
332         *                               algorithm specified by the header must
333         *                               be supported by the JWS signer. Must
334         *                               not be {@code null}.
335         * @param unprotectedHeader      The unprotected header to include,
336         *                               {@code null} if none.
337         * @param signer                 The JWS signer. Must not be
338         *                               {@code null}.
339         *
340         * @throws JOSEException If the JWS object couldn't be signed.
341         */
342        public synchronized void sign(final JWSHeader jwsHeader,
343                                      final UnprotectedHeader unprotectedHeader,
344                                      final JWSSigner signer)
345                throws JOSEException {
346                
347                try {
348                        HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
349                } catch (IllegalHeaderException e) {
350                        throw new IllegalArgumentException(e.getMessage(), e);
351                }
352                
353                JWSObject jwsObject = new JWSObject(jwsHeader, getPayload());
354                jwsObject.sign(signer);
355                
356                signatures.add(new Signature(getPayload(), jwsHeader, unprotectedHeader, jwsObject.getSignature()));
357        }
358        
359        
360        /**
361         * Returns the current signatures state.
362         *
363         * @return The state.
364         */
365        public State getState() {
366                
367                if (getSignatures().isEmpty()) {
368                        return State.UNSIGNED;
369                }
370                
371                for (Signature sig: getSignatures()) {
372                        if (! sig.isVerified()) {
373                                return State.SIGNED;
374                        }
375                }
376                
377                return State.VERIFIED;
378        }
379        
380        
381        @Override
382        public Map<String, Object> toGeneralJSONObject() {
383                
384                if (signatures.size() < 1) {
385                        throw new IllegalStateException("The general JWS JSON serialization requires at least one signature");
386                }
387                
388                Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
389                jsonObject.put("payload", getPayload().toBase64URL().toString());
390                
391                List<Object> signaturesJSONArray = JSONArrayUtils.newJSONArray();
392                
393                for (Signature signature: getSignatures()) {
394                        Map<String, Object> signatureJSONObject = signature.toJSONObject();
395                        signaturesJSONArray.add(signatureJSONObject);
396                }
397                
398                jsonObject.put("signatures", signaturesJSONArray);
399                
400                return jsonObject;
401        }
402        
403        
404        @Override
405        public Map<String, Object> toFlattenedJSONObject() {
406                
407                if (signatures.size() != 1) {
408                        throw new IllegalStateException("The flattened JWS JSON serialization requires exactly one signature");
409                }
410                
411                Map<String, Object> jsonObject = JSONObjectUtils.newJSONObject();
412                jsonObject.put("payload", getPayload().toBase64URL().toString());
413                jsonObject.putAll(getSignatures().get(0).toJSONObject());
414                return jsonObject;
415        }
416        
417        
418        @Override
419        public String serializeGeneral() {
420                return JSONObjectUtils.toJSONString(toGeneralJSONObject());
421        }
422        
423        
424        @Override
425        public String serializeFlattened() {
426                return JSONObjectUtils.toJSONString(toFlattenedJSONObject());
427        }
428        
429        
430        private static JWSHeader parseJWSHeader(final Map<String, Object> jsonObject)
431                throws ParseException {
432                
433                Base64URL protectedHeader = JSONObjectUtils.getBase64URL(jsonObject, "protected");
434                
435                if (protectedHeader == null) {
436                        throw new ParseException("Missing protected header (required by this library)", 0);
437                }
438                
439                try {
440                        return JWSHeader.parse(protectedHeader);
441                } catch (ParseException e) {
442                        if ("Not a JWS header".equals(e.getMessage())) {
443                                // alg required by this library (not the spec)
444                                throw new ParseException("Missing JWS \"alg\" parameter in protected header (required by this library)", 0);
445                        }
446                        throw e;
447                }
448        }
449        
450        
451        /**
452         * Parses a JWS secured object from the specified JSON object
453         * representation.
454         *
455         * @param jsonObject The JSON object to parse. Must not be
456         *                   {@code null}.
457         *
458         * @return The JWS secured object.
459         *
460         * @throws ParseException If the JSON object couldn't be parsed to a
461         *                        JWS secured object.
462         */
463        public static JWSObjectJSON parse(final Map<String, Object> jsonObject)
464                throws ParseException {
465                
466                // Payload always present
467                Base64URL payloadB64URL = JSONObjectUtils.getBase64URL(jsonObject, "payload");
468                
469                if (payloadB64URL == null) {
470                        throw new ParseException("Missing payload", 0);
471                }
472                
473                Payload payload = new Payload(payloadB64URL);
474                
475                // Signature present at top-level in flattened JSON
476                Base64URL topLevelSignatureB64 = JSONObjectUtils.getBase64URL(jsonObject, "signature");
477                
478                boolean flattened = topLevelSignatureB64 != null;
479                
480                List<Signature> signatureList = new LinkedList<>();
481                
482                if (flattened) {
483                        
484                        JWSHeader jwsHeader = parseJWSHeader(jsonObject);
485                        
486                        UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(jsonObject, "header"));
487                        
488                        // https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.2
489                        // "The "signatures" member MUST NOT be present when using this syntax."
490                        if (jsonObject.get("signatures") != null) {
491                                throw new ParseException("The \"signatures\" member must not be present in flattened JWS JSON serialization", 0);
492                        }
493                        
494                        try {
495                                HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
496                        } catch (IllegalHeaderException e) {
497                                throw new ParseException(e.getMessage(), 0);
498                        }
499                        
500                        signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, topLevelSignatureB64));
501                        
502                } else {
503                        Map<String, Object>[] signatures = JSONObjectUtils.getJSONObjectArray(jsonObject, "signatures");
504                        if (signatures == null || signatures.length == 0) {
505                                throw new ParseException("The \"signatures\" member must be present in general JSON Serialization", 0);
506                        }
507                        
508                        for (Map<String, Object> signatureJSONObject: signatures) {
509                                
510                                JWSHeader jwsHeader = parseJWSHeader(signatureJSONObject);
511                                
512                                UnprotectedHeader unprotectedHeader = UnprotectedHeader.parse(JSONObjectUtils.getJSONObject(signatureJSONObject, "header"));
513                                
514                                try {
515                                        HeaderValidation.ensureDisjoint(jwsHeader, unprotectedHeader);
516                                } catch (IllegalHeaderException e) {
517                                        throw new ParseException(e.getMessage(), 0);
518                                }
519                                
520                                Base64URL signatureB64 = JSONObjectUtils.getBase64URL(signatureJSONObject, "signature");
521                                
522                                if (signatureB64 == null) {
523                                        throw new ParseException("Missing \"signature\" member", 0);
524                                }
525                                
526                                signatureList.add(new Signature(payload, jwsHeader, unprotectedHeader, signatureB64));
527                        }
528                }
529                
530                return new JWSObjectJSON(payload, signatureList);
531        }
532        
533        
534        /**
535         * Parses a JWS secured object from the specified JSON object string.
536         *
537         * @param json The JSON object string to parse. Must not be
538         *             {@code null}.
539         *
540         * @return The JWS secured object.
541         *
542         * @throws ParseException If the string couldn't be parsed to a JWS
543         *                        secured object.
544         */
545        public static JWSObjectJSON parse(final String json)
546                throws ParseException {
547                
548                return parse(JSONObjectUtils.parse(json));
549        }
550}