Repository: googleapis/google-http-java-client
Pull request #1033 (2020-04-24 19:57:56)

feat: add logic for verifying ES256 JsonWebSignatures
Adds logic for verifying signatures that use NIST P-256 (ES256). It is up to the caller to provide the correct public key for verification.

Note that the difference between the ES256 and RS256 verification process is that ES256 requires AS1 (DER) notation for the signature so we do a limited conversion here before providing the signature to the signing algorithm.

Logic will be added to `google-auth-library` for handling Google's well-known, published public keys.


Patch:
diff --git a/google-http-client/src/main/java/com/google/api/client/json/webtoken/DerEncoder.java b/google-http-client/src/main/java/com/google/api/client/json/webtoken/DerEncoder.java
new file mode 100644
index 00000000..ce7c42f1
--- /dev/null
+++ b/google-http-client/src/main/java/com/google/api/client/json/webtoken/DerEncoder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 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
+ *
+ *     https://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.util.Preconditions;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+
+/**
+ * Utilities for re-encoding a signature byte array with DER encoding.
+ *
+ * <p>Note: that this is not a general purpose encoder and currently only
+ * handles 512 bit signatures. ES256 verification algorithms expect the
+ * signature bytes in DER encoding.
+ */
+public class DerEncoder {
+  private static byte DER_TAG_SIGNATURE_OBJECT = 0x30;
+  private static byte DER_TAG_ASN1_INTEGER = 0x02;
+
+  static byte[] encode(byte[] signature) {
+    // expect the signature to be 64 bytes long
+    Preconditions.checkState(signature.length == 64);
+
+    byte[] int1 = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)).toByteArray();
+    byte[] int2 = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)).toByteArray();
+    byte[] der = new byte[6 + int1.length + int2.length];
+
+    // Mark that this is a signature object
+    der[0] = DER_TAG_SIGNATURE_OBJECT;
+    der[1] = (byte) (der.length - 2);
+
+    // Start ASN1 integer and write the first 32 bits
+    der[2] = DER_TAG_ASN1_INTEGER;
+    der[3] = (byte) int1.length;
+    System.arraycopy(int1, 0, der, 4, int1.length);
+
+    // Start ASN1 integer and write the second 32 bits
+    int offset = int1.length + 4;
+    der[offset] = DER_TAG_ASN1_INTEGER;
+    der[offset + 1] = (byte) int2.length;
+    System.arraycopy(int2, 0, der, offset + 2, int2.length);
+
+    return der;
+  }
+}
diff --git a/google-http-client/src/main/java/com/google/api/client/json/webtoken/JsonWebSignature.java b/google-http-client/src/main/java/com/google/api/client/json/webtoken/JsonWebSignature.java
index 7e3990d4..aa21dbe2 100644
--- a/google-http-client/src/main/java/com/google/api/client/json/webtoken/JsonWebSignature.java
+++ b/google-http-client/src/main/java/com/google/api/client/json/webtoken/JsonWebSignature.java
@@ -23,6 +23,7 @@ import com.google.api.client.util.SecurityUtils;
 import com.google.api.client.util.StringUtils;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
@@ -349,30 +350,30 @@ public class JsonWebSignature extends JsonWebToken {
   /**
    * Verifies the signature of the content.
    *
-   * <p>Currently only {@code "RS256"} algorithm is verified, but others may be added in the future.
-   * For any other algorithm it returns {@code false}.
+   * <p>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 {
-    Signature signatureAlg = null;
     String algorithm = getHeader().getAlgorithm();
     if ("RS256".equals(algorithm)) {
-      signatureAlg = SecurityUtils.getSha256WithRsaSignatureAlgorithm();
+      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;
     }
-    return SecurityUtils.verify(signatureAlg, publicKey, signatureBytes, signedContentBytes);
   }
 
   /**
    * {@link Beta} <br>
    * Verifies the signature of the content using the certificate chain embedded in the signature.
    *
-   * <p>Currently only {@code "RS256"} algorithm is verified, but others may be added in the future.
-   * For any other algorithm it returns {@code null}.
+   * <p>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}.
    *
    * <p>The leaf certificate of the certificate chain must be an SSL server certificate.
    *
@@ -390,14 +391,13 @@ public class JsonWebSignature extends JsonWebToken {
       return null;
     }
     String algorithm = getHeader().getAlgorithm();
-    Signature signatureAlg = null;
     if ("RS256".equals(algorithm)) {
-      signatureAlg = SecurityUtils.getSha256WithRsaSignatureAlgorithm();
+      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;
     }
-    return SecurityUtils.verify(
-        signatureAlg, trustManager, x509Certificates, signatureBytes, signedContentBytes);
   }
 
   /**
diff --git a/google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java b/google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java
index 59d3af24..cf08e03a 100644
--- a/google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java
+++ b/google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java
@@ -127,6 +127,11 @@ public final class SecurityUtils {
     return Signature.getInstance("SHA256withRSA");
   }
 
+  /** Returns the SHA-256 with ECDSA signature algorithm */
+  public static Signature getEs256SignatureAlgorithm() throws NoSuchAlgorithmException {
+    return Signature.getInstance("SHA256withECDSA");
+  }
+
   /**
    * Signs content using a private key.
    *
@@ -157,7 +162,7 @@ public final class SecurityUtils {
       throws InvalidKeyException, SignatureException {
     signatureAlgorithm.initVerify(publicKey);
     signatureAlgorithm.update(contentBytes);
-    // SignatureException may be thrown if we are tring the wrong key.
+    // SignatureException may be thrown if we are trying the wrong key.
     try {
       return signatureAlgorithm.verify(signatureBytes);
     } catch (SignatureException e) {


Test patch:
diff --git a/google-http-client/src/test/java/com/google/api/client/json/webtoken/JsonWebSignatureTest.java b/google-http-client/src/test/java/com/google/api/client/json/webtoken/JsonWebSignatureTest.java
index 8bff77f9..9a02c075 100644
--- a/google-http-client/src/test/java/com/google/api/client/json/webtoken/JsonWebSignatureTest.java
+++ b/google-http-client/src/test/java/com/google/api/client/json/webtoken/JsonWebSignatureTest.java
@@ -19,14 +19,27 @@ import com.google.api.client.testing.json.webtoken.TestCertificates;
 import com.google.api.client.testing.util.SecurityTestUtils;
 
 import java.io.IOException;
+import java.math.BigInteger;
+import java.security.AlgorithmParameters;
 import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
 import java.security.cert.X509Certificate;
 import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.InvalidParameterSpecException;
 import java.util.ArrayList;
 import java.util.List;
 
 import javax.net.ssl.X509TrustManager;
 
+import com.google.api.client.util.Base64;
+import com.google.api.client.util.StringUtils;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -114,4 +127,38 @@ public class JsonWebSignatureTest {
   public void testVerifyX509WrongCa() throws Exception {
     Assert.assertNull(verifyX509WithCaCert(TestCertificates.BOGUS_CA_CERT));
   }
+
+  private static final String ES256_CONTENT = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ";
+  private static final String ES256_SIGNATURE = "yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA";
+
+  // x, y values for keyId "mpf0DA" from https://www.gstatic.com/iap/verify/public_key-jwk
+  private static final String GOOGLE_ES256_X = "fHEdeT3a6KaC1kbwov73ZwB_SiUHEyKQwUUtMCEn0aI";
+  private static final String GOOGLE_ES256_Y = "QWOjwPhInNuPlqjxLQyhveXpWqOFcQPhZ3t-koMNbZI";
+
+  private PublicKey buildEs256PublicKey(String x, String y)
+      throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
+    AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
+    parameters.init(new ECGenParameterSpec("secp256r1"));
+    ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(
+        new ECPoint(
+            new BigInteger(1, Base64.decodeBase64(x)),
+            new BigInteger(1, Base64.decodeBase64(y))
+        ),
+        parameters.getParameterSpec(ECParameterSpec.class)
+    );
+    KeyFactory keyFactory = KeyFactory.getInstance("EC");
+    return keyFactory.generatePublic(ecPublicKeySpec);
+  }
+
+  @Test
+  public void testVerifyES256() throws Exception {
+    PublicKey publicKey = buildEs256PublicKey(GOOGLE_ES256_X, GOOGLE_ES256_Y);
+    JsonWebSignature.Header header = new JsonWebSignature.Header();
+    header.setAlgorithm("ES256");
+    JsonWebSignature.Payload payload = new JsonWebToken.Payload();
+    byte[] signatureBytes = Base64.decodeBase64(ES256_SIGNATURE);
+    byte[] signedContentBytes = StringUtils.getBytesUtf8(ES256_CONTENT);
+    JsonWebSignature jsonWebSignature = new JsonWebSignature(header, payload, signatureBytes, signedContentBytes);
+    Assert.assertTrue(jsonWebSignature.verifySignature(publicKey));
+  }
 }