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$ (2012-10-23)
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                            throw new IllegalArgumentException("The JWS header must not be null");
089                    }
090    
091                    this.header = header;
092    
093                    if (payload == null) {
094                            throw new IllegalArgumentException("The payload must not be null");
095                    }
096    
097                    setPayload(payload);
098    
099                    setSignableContent(header.toBase64URL(), payload.toBase64URL());
100    
101                    signature = null;
102    
103                    state = State.UNSIGNED;
104            }
105    
106    
107            /**
108             * Creates a new signed JSON Web Signature (JWS) object with the 
109             * specified serialised parts. The state will be 
110             * {@link State#SIGNED signed}.
111             *
112             * @param firstPart  The first part, corresponding to the JWS header. 
113             *                   Must not be {@code null}.
114             * @param secondPart The second part, corresponding to the payload. Must
115             *                   not be {@code null}.
116             * @param thirdPart  The third part, corresponding to the signature.
117             *                   Must not be {@code null}.
118             *
119             * @throws ParseException If parsing of the serialised parts failed.
120             */
121            public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart)      
122                            throws ParseException {
123    
124                    if (firstPart == null) {
125                            throw new IllegalArgumentException("The first part must not be null");
126                    }
127    
128                    try {
129                            this.header = JWSHeader.parse(firstPart);
130    
131                    } catch (ParseException e) {
132    
133                            throw new ParseException("Invalid JWS header: " + e.getMessage(), 0);
134                    }
135    
136                    if (secondPart == null) {
137                            throw new IllegalArgumentException("The second part must not be null");
138                    }
139    
140                    setPayload(new Payload(secondPart));
141    
142                    setSignableContent(firstPart, secondPart);
143    
144                    if (thirdPart == null) {
145                            throw new IllegalArgumentException("The third part must not be null");
146                    }
147    
148                    signature = thirdPart;
149    
150                    state = State.SIGNED; // but signature not verified yet!
151    
152                    setParsedParts(firstPart, secondPart, thirdPart);
153            }
154    
155    
156            @Override
157            public ReadOnlyJWSHeader getHeader() {
158    
159                    return header;
160            }
161    
162    
163            /**
164             * Sets the signable content of this JWS object.
165             *
166             * <p>Format:
167             *
168             * <pre>
169             * [header-base64url].[payload-base64url]
170             * </pre>
171             *
172             * @param firstPart  The first part, corresponding to the JWS header.
173             *                   Must not be {@code null}.
174             * @param secondPart The second part, corresponding to the payload. Must
175             *                   not be {@code null}.
176             */
177            private void setSignableContent(final Base64URL firstPart, final Base64URL secondPart) {
178    
179                    StringBuilder sb = new StringBuilder(firstPart.toString());
180                    sb.append('.');
181                    sb.append(secondPart.toString());
182    
183                    try {
184                            signableContent = sb.toString().getBytes("UTF-8");
185    
186                    } catch (UnsupportedEncodingException e) {
187    
188                            // UTF-8 should always be supported
189                    }
190            }
191    
192    
193            /**
194             * Gets the signable content of this JWS object.
195             *
196             * <p>Format:
197             *
198             * <pre>
199             * [header-base64url].[payload-base64url]
200             * </pre>
201             *
202             * @return The signable content, ready for passing to the signing or
203             *         verification service.
204             */
205            public byte[] getSignableContent() {
206    
207                    return signableContent;
208            }
209    
210    
211            /**
212             * Gets the signature of this JWS object.
213             *
214             * @return The signature, {@code null} if the JWS object is not signed 
215             *         yet.
216             */
217            public Base64URL getSignature() {
218    
219                    return signature;
220            }
221    
222    
223            /**
224             * Gets the state of this JWS object.
225             *
226             * @return The state.
227             */
228            public State getState() {
229    
230                    return state;
231            }
232    
233    
234            /**
235             * Ensures the current state is {@link State#UNSIGNED unsigned}.
236             *
237             * @throws IllegalStateException If the current state is not unsigned.
238             */
239            private void ensureUnsignedState() {
240    
241                    if (state != State.UNSIGNED) {
242                            throw new IllegalStateException("The JWS object must be in an unsigned state");
243                    }
244            }
245    
246    
247            /**
248             * Ensures the current state is {@link State#SIGNED signed} or
249             * {@link State#VERIFIED verified}.
250             *
251             * @throws IllegalStateException If the current state is not signed or
252             *                               verified.
253             */
254            private void ensureSignedOrVerifiedState() {
255    
256                    if (state != State.SIGNED && state != State.VERIFIED) {
257                            throw new IllegalStateException("The JWS object must be in a signed or verified state");
258                    }
259            }
260    
261    
262            /**
263             * Ensures the specified JWS signer supports the algorithm of this JWS
264             * object.
265             *
266             * @throws JOSEException If the JWS algorithm is not supported.
267             */
268            private void ensureJWSSignerSupport(final JWSSigner signer)
269                            throws JOSEException {
270    
271                    if (! signer.supportedAlgorithms().contains(getHeader().getAlgorithm())) {
272    
273                            throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
274                                            "\" algorithm is not supported by the JWS signer");
275                    }
276            }
277    
278    
279            /**
280             * Ensures the specified JWS verifier accepts the algorithm and the headers 
281             * of this JWS object.
282             *
283             * @throws JOSEException If the JWS algorithm or headers are not accepted.
284             */
285            private void ensureJWSVerifierAcceptance(final JWSVerifier verifier)
286                            throws JOSEException {
287    
288                    JWSHeaderFilter filter = verifier.getJWSHeaderFilter();
289    
290                    if (filter == null) {
291                            return;
292                    }
293    
294                    if (! filter.getAcceptedAlgorithms().contains(getHeader().getAlgorithm())) {
295    
296                            throw new JOSEException("The \"" + getHeader().getAlgorithm() + 
297                                            "\" algorithm is not accepted by the JWS verifier");
298                    }
299    
300    
301                    if (! filter.getAcceptedParameters().containsAll(getHeader().getIncludedParameters())) {
302    
303                            throw new JOSEException("One or more header parameters not accepted by the JWS verifier");
304                    }
305            }
306    
307    
308            /**
309             * Signs this JWS object with the specified signer. The JWS object must
310             * be in a {@link State#UNSIGNED unsigned} state.
311             *
312             * @param signer The JWS signer. Must not be {@code null}.
313             *
314             * @throws IllegalStateException If the JWS object is not in an 
315             *                               {@link State#UNSIGNED unsigned state}.
316             * @throws JOSEException         If the JWS object couldn't be signed.
317             */
318            public synchronized void sign(final JWSSigner signer)
319                            throws JOSEException {
320    
321                    ensureUnsignedState();
322    
323                    ensureJWSSignerSupport(signer);
324    
325                    signature = signer.sign(getHeader(), getSignableContent());
326    
327                    state = State.SIGNED;
328            }
329    
330    
331            /**
332             * Checks the signature of this JWS object with the specified verifier. The
333             * JWS object must be in a {@link State#SIGNED signed} state.
334             *
335             * @param verifier The JWS verifier. Must not be {@code null}.
336             *
337             * @return {@code true} if the signature was successfully verified, else
338             *         {@code false}.
339             *
340             * @throws IllegalStateException If the JWS object is not in a 
341             *                               {@link State#SIGNED signed} or
342             *                               {@link State#VERIFIED verified state}.
343             * @throws JOSEException         If the JWS object couldn't be verified.
344             */
345            public synchronized boolean verify(final JWSVerifier verifier)
346                            throws JOSEException {
347    
348                    ensureSignedOrVerifiedState();
349    
350                    ensureJWSVerifierAcceptance(verifier);
351    
352                    boolean verified = verifier.verify(getHeader(), getSignableContent(), getSignature());
353    
354                    if (verified) {
355                            state = State.VERIFIED;
356                    }
357    
358                    return verified;
359            }
360    
361    
362            /**
363             * Serialises this JWS object to its compact format consisting of 
364             * Base64URL-encoded parts delimited by period ('.') characters. It must 
365             * be in a {@link State#SIGNED signed} or {@link State#VERIFIED verified} 
366             * state.
367             *
368             * <pre>
369             * [header-base64url].[payload-base64url].[signature-base64url]
370             * </pre>
371             *
372             * @return The serialised JWS object.
373             *
374             * @throws IllegalStateException If the JWS object is not in a 
375             *                               {@link State#SIGNED signed} or
376             *                               {@link State#VERIFIED verified} state.
377             */
378            @Override
379            public String serialize() {
380    
381                    ensureSignedOrVerifiedState();
382    
383                    StringBuilder sb = new StringBuilder(header.toBase64URL().toString());
384                    sb.append('.');
385                    sb.append(getPayload().toBase64URL().toString());
386                    sb.append('.');
387                    sb.append(signature.toString());
388                    return sb.toString();
389            }
390    
391    
392            /**
393             * Parses a JWS object from the specified string in compact format. The
394             * parsed JWS object will be given a {@link State#SIGNED} state.
395             *
396             * @param s The string to parse. Must not be {@code null}.
397             *
398             * @return The JWS object.
399             *
400             * @throws ParseException If the string couldn't be parsed to a valid JWS
401             *                        object.
402             */
403            public static JWSObject parse(String s)
404                            throws ParseException {
405    
406                    Base64URL[] parts = JOSEObject.split(s);
407    
408                    if (parts.length != 3) {
409                            throw new ParseException("Unexpected number of Base64URL parts, must be three", 0);
410                    }
411    
412                    return new JWSObject(parts[0], parts[1], parts[2]);
413            }
414    }