001/* 002 * nimbus-jose-jwt 003 * 004 * Copyright 2012-2016, Connect2id Ltd. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.jose; 019 020 021import java.text.ParseException; 022 023import com.nimbusds.jose.util.Base64URL; 024import com.nimbusds.jose.util.StandardCharset; 025import net.jcip.annotations.ThreadSafe; 026 027 028/** 029 * JSON Web Signature (JWS) secured object. This class is thread-safe. 030 * 031 * @author Vladimir Dzhuvinov 032 * @version 2019-08-01 033 */ 034@ThreadSafe 035public class JWSObject extends JOSEObject { 036 037 038 private static final long serialVersionUID = 1L; 039 040 041 /** 042 * Enumeration of the states of a JSON Web Signature (JWS) object. 043 */ 044 public enum State { 045 046 047 /** 048 * The JWS object is created but not signed yet. 049 */ 050 UNSIGNED, 051 052 053 /** 054 * The JWS object is signed but its signature is not verified. 055 */ 056 SIGNED, 057 058 059 /** 060 * The JWS object is signed and its signature was successfully verified. 061 */ 062 VERIFIED 063 } 064 065 066 /** 067 * The header. 068 */ 069 private final JWSHeader header; 070 071 072 /** 073 * The signing input for this JWS object. 074 * 075 * <p>Format: 076 * 077 * <pre> 078 * [header-base64url].[payload-base64url] 079 * </pre> 080 */ 081 private final String signingInputString; 082 083 084 /** 085 * The signature, {@code null} if not signed. 086 */ 087 private Base64URL signature; 088 089 090 /** 091 * The JWS object state. 092 */ 093 private State state; 094 095 096 /** 097 * Creates a new to-be-signed JSON Web Signature (JWS) object with the 098 * specified header and payload. The initial state will be 099 * {@link State#UNSIGNED unsigned}. 100 * 101 * @param header The JWS header. Must not be {@code null}. 102 * @param payload The payload. Must not be {@code null}. 103 */ 104 public JWSObject(final JWSHeader header, final Payload payload) { 105 106 if (header == null) { 107 108 throw new IllegalArgumentException("The JWS header must not be null"); 109 } 110 111 this.header = header; 112 113 if (payload == null) { 114 115 throw new IllegalArgumentException("The payload must not be null"); 116 } 117 118 setPayload(payload); 119 120 if (header.getCustomParam("b64") == null || (Boolean) header.getCustomParam("b64")) { 121 signingInputString = composeSigningInput(header.toBase64URL(), payload.toBase64URL()); 122 } else { 123 signingInputString = header.toBase64URL().toString() + '.' + payload.toString(); 124 } 125 signature = null; 126 127 state = State.UNSIGNED; 128 } 129 130 131 /** 132 * Creates a new signed JSON Web Signature (JWS) object with the 133 * specified serialised parts. The state will be 134 * {@link State#SIGNED signed}. 135 * 136 * @param firstPart The first part, corresponding to the JWS header. 137 * Must not be {@code null}. 138 * @param secondPart The second part, corresponding to the payload. Must 139 * not be {@code null}. 140 * @param thirdPart The third part, corresponding to the signature. 141 * Must not be {@code null}. 142 * 143 * @throws ParseException If parsing of the serialised parts failed. 144 */ 145 public JWSObject(final Base64URL firstPart, final Base64URL secondPart, final Base64URL thirdPart) 146 throws ParseException { 147 148 if (firstPart == null) { 149 150 throw new IllegalArgumentException("The first part must not be null"); 151 } 152 153 try { 154 this.header = JWSHeader.parse(firstPart); 155 156 } catch (ParseException e) { 157 158 throw new ParseException("Invalid JWS header: " + e.getMessage(), 0); 159 } 160 161 if (secondPart == null) { 162 163 throw new IllegalArgumentException("The second part must not be null"); 164 } 165 166 setPayload(new Payload(secondPart)); 167 168 signingInputString = composeSigningInput(firstPart, secondPart); 169 170 if (thirdPart == null) { 171 throw new IllegalArgumentException("The third part must not be null"); 172 } 173 174 signature = thirdPart; 175 176 state = State.SIGNED; // but signature not verified yet! 177 178 setParsedParts(firstPart, secondPart, thirdPart); 179 } 180 181 182 @Override 183 public JWSHeader getHeader() { 184 185 return header; 186 } 187 188 189 /** 190 * Composes the signing input for the specified JWS object parts. 191 * 192 * <p>Format: 193 * 194 * <pre> 195 * [header-base64url].[payload-base64url] 196 * </pre> 197 * 198 * @param firstPart The first part, corresponding to the JWS header. 199 * Must not be {@code null}. 200 * @param secondPart The second part, corresponding to the payload. 201 * Must not be {@code null}. 202 * 203 * @return The signing input string. 204 */ 205 private static String composeSigningInput(final Base64URL firstPart, final Base64URL secondPart) { 206 207 return firstPart.toString() + '.' + secondPart.toString(); 208 } 209 210 211 /** 212 * Returns the signing input for this JWS object. 213 * 214 * <p>Format: 215 * 216 * <pre> 217 * [header-base64url].[payload-base64url] 218 * </pre> 219 * 220 * @return The signing input, to be passed to a JWS signer or verifier. 221 */ 222 public byte[] getSigningInput() { 223 224 return signingInputString.getBytes(StandardCharset.UTF_8); 225 } 226 227 228 /** 229 * Returns the signature of this JWS object. 230 * 231 * @return The signature, {@code null} if the JWS object is not signed 232 * yet. 233 */ 234 public Base64URL getSignature() { 235 236 return signature; 237 } 238 239 240 /** 241 * Returns the state of this JWS object. 242 * 243 * @return The state. 244 */ 245 public State getState() { 246 247 return state; 248 } 249 250 251 /** 252 * Ensures the current state is {@link State#UNSIGNED unsigned}. 253 * 254 * @throws IllegalStateException If the current state is not unsigned. 255 */ 256 private void ensureUnsignedState() { 257 258 if (state != State.UNSIGNED) { 259 260 throw new IllegalStateException("The JWS object must be in an unsigned state"); 261 } 262 } 263 264 265 /** 266 * Ensures the current state is {@link State#SIGNED signed} or 267 * {@link State#VERIFIED verified}. 268 * 269 * @throws IllegalStateException If the current state is not signed or 270 * verified. 271 */ 272 private void ensureSignedOrVerifiedState() { 273 274 if (state != State.SIGNED && state != State.VERIFIED) { 275 276 throw new IllegalStateException("The JWS object must be in a signed or verified state"); 277 } 278 } 279 280 281 /** 282 * Ensures the specified JWS signer supports the algorithm of this JWS 283 * object. 284 * 285 * @throws JOSEException If the JWS algorithm is not supported. 286 */ 287 private void ensureJWSSignerSupport(final JWSSigner signer) 288 throws JOSEException { 289 290 if (! signer.supportedJWSAlgorithms().contains(getHeader().getAlgorithm())) { 291 292 throw new JOSEException("The \"" + getHeader().getAlgorithm() + 293 "\" algorithm is not allowed or supported by the JWS signer: Supported algorithms: " + signer.supportedJWSAlgorithms()); 294 } 295 } 296 297 298 /** 299 * Signs this JWS object with the specified signer. The JWS object must 300 * be in a {@link State#UNSIGNED unsigned} state. 301 * 302 * @param signer The JWS signer. Must not be {@code null}. 303 * 304 * @throws IllegalStateException If the JWS object is not in an 305 * {@link State#UNSIGNED unsigned state}. 306 * @throws JOSEException If the JWS object couldn't be signed. 307 */ 308 public synchronized void sign(final JWSSigner signer) 309 throws JOSEException { 310 311 ensureUnsignedState(); 312 313 ensureJWSSignerSupport(signer); 314 315 try { 316 signature = signer.sign(getHeader(), getSigningInput()); 317 318 } catch (JOSEException e) { 319 320 throw e; 321 322 } catch (Exception e) { 323 324 // Prevent throwing unchecked exceptions at this point, 325 // see issue #20 326 throw new JOSEException(e.getMessage(), e); 327 } 328 329 state = State.SIGNED; 330 } 331 332 333 /** 334 * Checks the signature of this JWS object with the specified verifier. 335 * The JWS object must be in a {@link State#SIGNED signed} state. 336 * 337 * @param verifier The JWS verifier. Must not be {@code null}. 338 * 339 * @return {@code true} if the signature was successfully verified, 340 * else {@code false}. 341 * 342 * @throws IllegalStateException If the JWS object is not in a 343 * {@link State#SIGNED signed} or 344 * {@link State#VERIFIED verified state}. 345 * @throws JOSEException If the JWS object couldn't be 346 * verified. 347 */ 348 public synchronized boolean verify(final JWSVerifier verifier) 349 throws JOSEException { 350 351 ensureSignedOrVerifiedState(); 352 353 boolean verified; 354 355 try { 356 verified = verifier.verify(getHeader(), getSigningInput(), getSignature()); 357 358 } catch (JOSEException e) { 359 360 throw e; 361 362 } catch (Exception e) { 363 364 // Prevent throwing unchecked exceptions at this point, 365 // see issue #20 366 throw new JOSEException(e.getMessage(), e); 367 } 368 369 if (verified) { 370 371 state = State.VERIFIED; 372 } 373 374 return verified; 375 } 376 377 378 /** 379 * Serialises this JWS object to its compact format consisting of 380 * Base64URL-encoded parts delimited by period ('.') characters. It 381 * must be in a {@link State#SIGNED signed} or 382 * {@link State#VERIFIED verified} state. 383 * 384 * <pre> 385 * [header-base64url].[payload-base64url].[signature-base64url] 386 * </pre> 387 * 388 * @return The serialised JWS object. 389 * 390 * @throws IllegalStateException If the JWS object is not in a 391 * {@link State#SIGNED signed} or 392 * {@link State#VERIFIED verified} state. 393 */ 394 @Override 395 public String serialize() { 396 return serialize(false); 397 } 398 399 400 /** 401 * Serialises this JWS object to its compact format consisting of 402 * Base64URL-encoded parts delimited by period ('.') characters. It 403 * must be in a {@link State#SIGNED signed} or 404 * {@link State#VERIFIED verified} state. 405 * 406 * @param detachedPayload {@code true} to return a serialised object 407 * with a detached payload compliant with RFC 408 * 7797, {@code false} for regular JWS 409 * serialisation. 410 * 411 * @return The serialised JOSE object. 412 * 413 * @throws IllegalStateException If the JOSE object is not in a state 414 * that permits serialisation. 415 */ 416 public String serialize(final boolean detachedPayload) { 417 ensureSignedOrVerifiedState(); 418 419 if (detachedPayload) { 420 return header.toBase64URL().toString() + '.' + '.' + signature.toString(); 421 } 422 423 return signingInputString + '.' + signature.toString(); 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}