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