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 000000000..ce7c42f12 --- /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. + * + *

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 7e3990d40..aa21dbe2e 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.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 Header getHeader() { /** * Verifies the signature of the content. * - *

Currently only {@code "RS256"} algorithm is verified, but others may be added in the future. - * For any other algorithm it returns {@code false}. + *

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}
* 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}. + *

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. * @@ -390,14 +391,13 @@ public final X509Certificate verifySignature(X509TrustManager trustManager) 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 59d3af24e..cf08e03ad 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 static Signature getSha256WithRsaSignatureAlgorithm() throws NoSuchAlgori 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 static boolean verify( 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) { 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 8bff77f93..9a02c0750 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.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 void testVerifyX509() throws Exception { 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)); + } }