com.google.api.client.json.webtoken.JsonWebSignature Maven / Gradle / Ivy
Show all versions of google-http-client Show documentation
/*
* Copyright 2012 Google LLC.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.api.client.json.webtoken;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.Base64;
import com.google.api.client.util.Beta;
import com.google.api.client.util.Key;
import com.google.api.client.util.Preconditions;
import com.google.api.client.util.SecurityUtils;
import com.google.api.client.util.StringUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
/**
* JSON Web Signature(JWS).
*
* Sample usage:
*
*
* public static void printPayload(JsonFactory jsonFactory, String tokenString) throws IOException {
* JsonWebSignature jws = JsonWebSignature.parse(jsonFactory, tokenString);
* System.out.println(jws.getPayload());
* }
*
*
* Implementation is not thread-safe.
*
* @since 1.14 (since 1.7 as com.google.api.client.auth.jsontoken.JsonWebSignature)
* @author Yaniv Inbar
*/
public class JsonWebSignature extends JsonWebToken {
/** Bytes of the signature. */
private final byte[] signatureBytes;
/** Bytes of the signature content. */
private final byte[] signedContentBytes;
/**
* @param header header
* @param payload payload
* @param signatureBytes bytes of the signature
* @param signedContentBytes bytes of the signed content
*/
public JsonWebSignature(
Header header, Payload payload, byte[] signatureBytes, byte[] signedContentBytes) {
super(header, payload);
this.signatureBytes = Preconditions.checkNotNull(signatureBytes);
this.signedContentBytes = Preconditions.checkNotNull(signedContentBytes);
}
/**
* Header as specified in Reserved
* Header Parameter Names.
*/
public static class Header extends JsonWebToken.Header {
/**
* Algorithm header parameter that identifies the cryptographic algorithm used to secure the JWS
* or {@code null} for none.
*/
@Key("alg")
private String algorithm;
/**
* JSON Web Key URL header parameter that is an absolute URL that refers to a resource for a set
* of JSON-encoded public keys, one of which corresponds to the key that was used to digitally
* sign the JWS or {@code null} for none.
*/
@Key("jku")
private String jwkUrl;
/**
* JSON Web Key header parameter that is a public key that corresponds to the key used to
* digitally sign the JWS or {@code null} for none.
*/
@Key("jwk")
private String jwk;
/**
* Key ID header parameter that is a hint indicating which specific key owned by the signer
* should be used to validate the digital signature or {@code null} for none.
*/
@Key("kid")
private String keyId;
/**
* X.509 URL header parameter that is an absolute URL that refers to a resource for the X.509
* public key certificate or certificate chain corresponding to the key used to digitally sign
* the JWS or {@code null} for none.
*/
@Key("x5u")
private String x509Url;
/**
* X.509 certificate thumbprint header parameter that provides a base64url encoded SHA-1
* thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate that can be used to
* match the certificate or {@code null} for none.
*/
@Key("x5t")
private String x509Thumbprint;
/**
* X.509 certificate chain header parameter contains the X.509 public key certificate or
* certificate chain corresponding to the key used to digitally sign the JWS or {@code null} for
* none.
*
* @since 1.19.1.
*/
@Key("x5c")
private ArrayList x509Certificates;
/**
* Array listing the header parameter names that define extensions that are used in the JWS
* header that MUST be understood and processed or {@code null} for none.
*/
@Key("crit")
private List critical;
@Override
public Header setType(String type) {
super.setType(type);
return this;
}
/**
* Returns the algorithm header parameter that identifies the cryptographic algorithm used to
* secure the JWS or {@code null} for none.
*/
public final String getAlgorithm() {
return algorithm;
}
/**
* Sets the algorithm header parameter that identifies the cryptographic algorithm used to
* secure the JWS or {@code null} for none.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setAlgorithm(String algorithm) {
this.algorithm = algorithm;
return this;
}
/**
* Returns the JSON Web Key URL header parameter that is an absolute URL that refers to a
* resource for a set of JSON-encoded public keys, one of which corresponds to the key that was
* used to digitally sign the JWS or {@code null} for none.
*/
public final String getJwkUrl() {
return jwkUrl;
}
/**
* Sets the JSON Web Key URL header parameter that is an absolute URL that refers to a resource
* for a set of JSON-encoded public keys, one of which corresponds to the key that was used to
* digitally sign the JWS or {@code null} for none.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setJwkUrl(String jwkUrl) {
this.jwkUrl = jwkUrl;
return this;
}
/**
* Returns the JSON Web Key header parameter that is a public key that corresponds to the key
* used to digitally sign the JWS or {@code null} for none.
*/
public final String getJwk() {
return jwk;
}
/**
* Sets the JSON Web Key header parameter that is a public key that corresponds to the key used
* to digitally sign the JWS or {@code null} for none.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setJwk(String jwk) {
this.jwk = jwk;
return this;
}
/**
* Returns the key ID header parameter that is a hint indicating which specific key owned by the
* signer should be used to validate the digital signature or {@code null} for none.
*/
public final String getKeyId() {
return keyId;
}
/**
* Sets the key ID header parameter that is a hint indicating which specific key owned by the
* signer should be used to validate the digital signature or {@code null} for none.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setKeyId(String keyId) {
this.keyId = keyId;
return this;
}
/**
* Returns the X.509 URL header parameter that is an absolute URL that refers to a resource for
* the X.509 public key certificate or certificate chain corresponding to the key used to
* digitally sign the JWS or {@code null} for none.
*/
public final String getX509Url() {
return x509Url;
}
/**
* Sets the X.509 URL header parameter that is an absolute URL that refers to a resource for the
* X.509 public key certificate or certificate chain corresponding to the key used to digitally
* sign the JWS or {@code null} for none.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setX509Url(String x509Url) {
this.x509Url = x509Url;
return this;
}
/**
* Returns the X.509 certificate thumbprint header parameter that provides a base64url encoded
* SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate that can be used
* to match the certificate or {@code null} for none.
*/
public final String getX509Thumbprint() {
return x509Thumbprint;
}
/**
* Sets the X.509 certificate thumbprint header parameter that provides a base64url encoded
* SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate that can be used
* to match the certificate or {@code null} for none.
*
*
Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*/
public Header setX509Thumbprint(String x509Thumbprint) {
this.x509Thumbprint = x509Thumbprint;
return this;
}
/**
* Returns the X.509 certificate chain header parameter contains the X.509 public key
* certificate or certificate chain corresponding to the key used to digitally sign the JWS or
* {@code null} for none.
*
* @since 1.19.1.
*/
public final List getX509Certificates() {
return new ArrayList<>(x509Certificates);
}
/**
* Sets the X.509 certificate chain header parameter contains the X.509 public key certificate
* or certificate chain corresponding to the key used to digitally sign the JWS or {@code null}
* for none.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*
* @since 1.19.1.
*/
public Header setX509Certificates(List x509Certificates) {
this.x509Certificates = new ArrayList<>(x509Certificates);
return this;
}
/**
* Returns an array listing the header parameter names that define extensions used in the JWS
* header that MUST be understood and processed or {@code null} for none.
*
* @since 1.16
*/
public final List getCritical() {
if (critical == null || critical.isEmpty()) {
return null;
}
return new ArrayList<>(critical);
}
/**
* Sets the header parameter names that define extensions used in the JWS header that MUST be
* understood and processed or {@code null} for none.
*
* Overriding is only supported for the purpose of calling the super implementation and
* changing the return type, but nothing else.
*
* @since 1.16
*/
public Header setCritical(List critical) {
this.critical = new ArrayList<>(critical);
return this;
}
@Override
public Header set(String fieldName, Object value) {
return (Header) super.set(fieldName, value);
}
@Override
public Header clone() {
return (Header) super.clone();
}
}
@Override
public Header getHeader() {
return (Header) super.getHeader();
}
/**
* Verifies the signature of the content.
*
* Currently only {@code "RS256"} and {@code "ES256"} algorithms are verified, but others may
* be added in the future. For any other algorithm it returns {@code false}.
*
* @param publicKey public key
* @return whether the algorithm is recognized and it is verified
* @throws GeneralSecurityException
*/
public final boolean verifySignature(PublicKey publicKey) throws GeneralSecurityException {
String algorithm = getHeader().getAlgorithm();
if ("RS256".equals(algorithm)) {
return SecurityUtils.verify(
SecurityUtils.getSha256WithRsaSignatureAlgorithm(),
publicKey,
signatureBytes,
signedContentBytes);
} else if ("ES256".equals(algorithm)) {
return SecurityUtils.verify(
SecurityUtils.getEs256SignatureAlgorithm(),
publicKey,
DerEncoder.encode(signatureBytes),
signedContentBytes);
} else {
return false;
}
}
/**
* {@link Beta}
* Verifies the signature of the content using the certificate chain embedded in the signature.
*
*
Currently only {@code "RS256"} and {@code "ES256"} algorithms are verified, but others may
* be added in the future. For any other algorithm it returns {@code null}.
*
*
The leaf certificate of the certificate chain must be an SSL server certificate.
*
* @param trustManager trust manager used to verify the X509 certificate chain embedded in this
* message
* @return the signature certificate if the signature could be verified, null otherwise
* @throws GeneralSecurityException
* @since 1.19.1
*/
@Beta
public final X509Certificate verifySignature(X509TrustManager trustManager)
throws GeneralSecurityException {
List x509Certificates = getHeader().getX509Certificates();
if (x509Certificates == null || x509Certificates.isEmpty()) {
return null;
}
String algorithm = getHeader().getAlgorithm();
if ("RS256".equals(algorithm)) {
return SecurityUtils.verify(
SecurityUtils.getSha256WithRsaSignatureAlgorithm(),
trustManager,
x509Certificates,
signatureBytes,
signedContentBytes);
} else if ("ES256".equals(algorithm)) {
return SecurityUtils.verify(
SecurityUtils.getEs256SignatureAlgorithm(),
trustManager,
x509Certificates,
DerEncoder.encode(signatureBytes),
signedContentBytes);
} else {
return null;
}
}
/**
* {@link Beta}
* Verifies the signature of the content using the certificate chain embedded in the signature.
*
* Currently only {@code "RS256"} algorithm is verified, but others may be added in the future.
* For any other algorithm it returns {@code null}.
*
*
The certificate chain is verified using the system default trust manager.
*
*
The leaf certificate of the certificate chain must be an SSL server certificate.
*
* @return the signature certificate if the signature could be verified, null otherwise
* @throws GeneralSecurityException
* @since 1.19.1.
*/
@Beta
public final X509Certificate verifySignature() throws GeneralSecurityException {
X509TrustManager trustManager = getDefaultX509TrustManager();
if (trustManager == null) {
return null;
}
return verifySignature(trustManager);
}
private static X509TrustManager getDefaultX509TrustManager() {
try {
TrustManagerFactory factory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init((KeyStore) null);
for (TrustManager manager : factory.getTrustManagers()) {
if (manager instanceof X509TrustManager) {
return (X509TrustManager) manager;
}
}
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (KeyStoreException e) {
return null;
}
}
/** Returns the bytes of the signature. */
public final byte[] getSignatureBytes() {
return Arrays.copyOf(signatureBytes, signatureBytes.length);
}
/** Returns the bytes of the signature content. */
public final byte[] getSignedContentBytes() {
return Arrays.copyOf(signedContentBytes, signedContentBytes.length);
}
/**
* Parses the given JWS token string and returns the parsed {@link JsonWebSignature}.
*
* @param jsonFactory JSON factory
* @param tokenString JWS token string
* @return parsed JWS
*/
public static JsonWebSignature parse(JsonFactory jsonFactory, String tokenString)
throws IOException {
return parser(jsonFactory).parse(tokenString);
}
/** Returns a new instance of a JWS parser. */
public static Parser parser(JsonFactory jsonFactory) {
return new Parser(jsonFactory);
}
/**
* JWS parser.
*
*
Implementation is not thread-safe.
*/
public static final class Parser {
/** JSON factory. */
private final JsonFactory jsonFactory;
/** Header class to use for parsing. */
private Class extends Header> headerClass = Header.class;
/** Payload class to use for parsing. */
private Class extends Payload> payloadClass = Payload.class;
/** @param jsonFactory JSON factory */
public Parser(JsonFactory jsonFactory) {
this.jsonFactory = Preconditions.checkNotNull(jsonFactory);
}
/** Returns the header class to use for parsing. */
public Class extends Header> getHeaderClass() {
return headerClass;
}
/** Sets the header class to use for parsing. */
public Parser setHeaderClass(Class extends Header> headerClass) {
this.headerClass = headerClass;
return this;
}
/** Returns the payload class to use for parsing. */
public Class extends Payload> getPayloadClass() {
return payloadClass;
}
/** Sets the payload class to use for parsing. */
public Parser setPayloadClass(Class extends Payload> payloadClass) {
this.payloadClass = payloadClass;
return this;
}
/** Returns the JSON factory. */
public JsonFactory getJsonFactory() {
return jsonFactory;
}
/**
* Parses a JWS token into a parsed {@link JsonWebSignature}.
*
* @param tokenString JWS token string
* @return parsed {@link JsonWebSignature}
*/
public JsonWebSignature parse(String tokenString) throws IOException {
// split on the dots
int firstDot = tokenString.indexOf('.');
Preconditions.checkArgument(firstDot != -1);
byte[] headerBytes = Base64.decodeBase64(tokenString.substring(0, firstDot));
int secondDot = tokenString.indexOf('.', firstDot + 1);
Preconditions.checkArgument(secondDot != -1);
Preconditions.checkArgument(tokenString.indexOf('.', secondDot + 1) == -1);
// decode the bytes
byte[] payloadBytes = Base64.decodeBase64(tokenString.substring(firstDot + 1, secondDot));
byte[] signatureBytes = Base64.decodeBase64(tokenString.substring(secondDot + 1));
byte[] signedContentBytes = StringUtils.getBytesUtf8(tokenString.substring(0, secondDot));
// parse the header and payload
Header header =
jsonFactory.fromInputStream(new ByteArrayInputStream(headerBytes), headerClass);
Preconditions.checkArgument(header.getAlgorithm() != null);
Payload payload =
jsonFactory.fromInputStream(new ByteArrayInputStream(payloadBytes), payloadClass);
return new JsonWebSignature(header, payload, signatureBytes, signedContentBytes);
}
}
/**
* Signs a given JWS header and payload based on the given private key using RSA and SHA-256 as
* described in JWS using
* RSA SHA-256.
*
* @param privateKey private key
* @param jsonFactory JSON factory
* @param header JWS header
* @param payload JWS payload
* @return signed JWS string
* @since 1.14 (since 1.7 as com.google.api.client.auth.jsontoken.RsaSHA256Signer)
*/
public static String signUsingRsaSha256(
PrivateKey privateKey,
JsonFactory jsonFactory,
JsonWebSignature.Header header,
JsonWebToken.Payload payload)
throws GeneralSecurityException, IOException {
String content =
Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header))
+ "."
+ Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload));
byte[] contentBytes = StringUtils.getBytesUtf8(content);
byte[] signature =
SecurityUtils.sign(
SecurityUtils.getSha256WithRsaSignatureAlgorithm(), privateKey, contentBytes);
return content + "." + Base64.encodeBase64URLSafeString(signature);
}
}