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