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