001    package com.nimbusds.jose;
002    
003    
004    import java.nio.charset.Charset;
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-16)
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                    signingInput = sb.toString().getBytes(Charset.forName("UTF-8"));
216            }
217    
218    
219            /**
220             * @deprecated Use {@link #setSigningInput} instead.
221             */
222            private void setSignableContent(final Base64URL firstPart, final Base64URL secondPart) {
223    
224                    setSigningInput(firstPart, secondPart);
225            }
226    
227    
228            /**
229             * Gets the signing input for this JWS object.
230             *
231             * <p>Format:
232             *
233             * <pre>
234             * [header-base64url].[payload-base64url]
235             * </pre>
236             *
237             * @return The signing input, to be passed to a JWS signer or verifier.
238             */
239            public byte[] getSigningInput() {
240    
241                    return signingInput;
242            }
243    
244    
245            /**
246             * @deprecated Use {@link #getSigningInput} instead.
247             */
248            @Deprecated
249            public byte[] getSignableContent() {
250    
251                    return getSigningInput();
252            }
253    
254    
255            /**
256             * Gets the signature of this JWS object.
257             *
258             * @return The signature, {@code null} if the JWS object is not signed 
259             *         yet.
260             */
261            public Base64URL getSignature() {
262    
263                    return signature;
264            }
265    
266    
267            /**
268             * Gets the state of this JWS object.
269             *
270             * @return The state.
271             */
272            public State getState() {
273    
274                    return state;
275            }
276    
277    
278            /**
279             * Ensures the current state is {@link State#UNSIGNED unsigned}.
280             *
281             * @throws IllegalStateException If the current state is not unsigned.
282             */
283            private void ensureUnsignedState() {
284    
285                    if (state != State.UNSIGNED) {
286    
287                            throw new IllegalStateException("The JWS object must be in an unsigned state");
288                    }
289            }
290    
291    
292            /**
293             * Ensures the current state is {@link State#SIGNED signed} or
294             * {@link State#VERIFIED verified}.
295             *
296             * @throws IllegalStateException If the current state is not signed or
297             *                               verified.
298             */
299            private void ensureSignedOrVerifiedState() {
300    
301                    if (state != State.SIGNED && state != State.VERIFIED) {
302    
303                            throw new IllegalStateException("The JWS object must be in a signed or verified state");
304                    }
305            }
306    
307    
308            /**
309             * Ensures the specified JWS signer supports the algorithm of this JWS
310             * object.
311             *
312             * @throws JOSEException If the JWS algorithm is not supported.
313             */
314            private void ensureJWSSignerSupport(final JWSSigner signer)
315                    throws JOSEException {
316    
317                    if (! signer.supportedAlgorithms().contains(getHeader().getAlgorithm())) {
318    
319                            throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
320                                                    "\" algorithm is not supported by the JWS signer");
321                    }
322            }
323    
324    
325            /**
326             * Ensures the specified JWS verifier accepts the algorithm and the headers 
327             * of this JWS object.
328             *
329             * @throws JOSEException If the JWS algorithm or headers are not accepted.
330             */
331            private void ensureJWSVerifierAcceptance(final JWSVerifier verifier)
332                    throws JOSEException {
333    
334                    JWSHeaderFilter filter = verifier.getJWSHeaderFilter();
335    
336                    if (filter == null) {
337                            return;
338                    }
339    
340                    if (! filter.getAcceptedAlgorithms().contains(getHeader().getAlgorithm())) {
341    
342                            throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
343                                            "\" algorithm is not accepted by the JWS verifier");
344                    }
345    
346    
347                    if (! filter.getAcceptedParameters().containsAll(getHeader().getIncludedParameters())) {
348    
349                            throw new JOSEException("One or more header parameters not accepted by the JWS verifier");
350                    }
351            }
352    
353    
354            /**
355             * Signs this JWS object with the specified signer. The JWS object must
356             * be in a {@link State#UNSIGNED unsigned} state.
357             *
358             * @param signer The JWS signer. Must not be {@code null}.
359             *
360             * @throws IllegalStateException If the JWS object is not in an 
361             *                               {@link State#UNSIGNED unsigned state}.
362             * @throws JOSEException         If the JWS object couldn't be signed.
363             */
364            public synchronized void sign(final JWSSigner signer)
365                    throws JOSEException {
366    
367                    ensureUnsignedState();
368    
369                    ensureJWSSignerSupport(signer);
370    
371                    try {
372                            signature = signer.sign(getHeader(), getSigningInput());
373    
374                    } catch (JOSEException e) {
375    
376                            throw e;
377                                    
378                    } catch (Exception e) {
379    
380                            // Prevent throwing unchecked exceptions at this point,
381                            // see issue #20
382                            throw new JOSEException(e.getMessage(), e);
383                    }
384                    
385    
386                    state = State.SIGNED;
387            }
388    
389    
390            /**
391             * Checks the signature of this JWS object with the specified verifier. 
392             * The JWS object must be in a {@link State#SIGNED signed} state.
393             *
394             * @param verifier The JWS verifier. Must not be {@code null}.
395             *
396             * @return {@code true} if the signature was successfully verified, 
397             *         else {@code false}.
398             *
399             * @throws IllegalStateException If the JWS object is not in a 
400             *                               {@link State#SIGNED signed} or
401             *                               {@link State#VERIFIED verified state}.
402             * @throws JOSEException         If the JWS object couldn't be verified.
403             */
404            public synchronized boolean verify(final JWSVerifier verifier)
405                    throws JOSEException {
406    
407                    ensureSignedOrVerifiedState();
408    
409                    ensureJWSVerifierAcceptance(verifier);
410    
411                    boolean verified = false;
412    
413                    try {
414                            verified = verifier.verify(getHeader(), getSigningInput(), getSignature());
415    
416                    } catch (JOSEException e) {
417    
418                            throw e;
419    
420                    } catch (Exception e) {
421    
422                            // Prevent throwing unchecked exceptions at this point,
423                            // see issue #20
424                            throw new JOSEException(e.getMessage(), e);
425                    }
426    
427                    if (verified) {
428    
429                            state = State.VERIFIED;
430                    }
431    
432                    return verified;
433            }
434    
435    
436            /**
437             * Serialises this JWS object to its compact format consisting of 
438             * Base64URL-encoded parts delimited by period ('.') characters. It 
439             * must be in a {@link State#SIGNED signed} or 
440             * {@link State#VERIFIED verified} state.
441             *
442             * <pre>
443             * [header-base64url].[payload-base64url].[signature-base64url]
444             * </pre>
445             *
446             * @return The serialised JWS object.
447             *
448             * @throws IllegalStateException If the JWS object is not in a 
449             *                               {@link State#SIGNED signed} or
450             *                               {@link State#VERIFIED verified} state.
451             */
452            @Override
453            public String serialize() {
454    
455                    ensureSignedOrVerifiedState();
456    
457                    StringBuilder sb = new StringBuilder(header.toBase64URL().toString());
458                    sb.append('.');
459                    sb.append(getPayload().toBase64URL().toString());
460                    sb.append('.');
461                    sb.append(signature.toString());
462                    return sb.toString();
463            }
464    
465    
466            /**
467             * Parses a JWS object from the specified string in compact format. The
468             * parsed JWS object will be given a {@link State#SIGNED} state.
469             *
470             * @param s The string to parse. Must not be {@code null}.
471             *
472             * @return The JWS object.
473             *
474             * @throws ParseException If the string couldn't be parsed to a valid 
475             *                        JWS object.
476             */
477            public static JWSObject parse(final String s)
478                    throws ParseException {
479    
480                    Base64URL[] parts = JOSEObject.split(s);
481    
482                    if (parts.length != 3) {
483    
484                            throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
485                    }
486    
487                    return new JWSObject(parts[0], parts[1], parts[2]);
488            }
489    }