Skip to content

Commit bb4227f

Browse files
authored
feat: add logic for verifying ES256 JsonWebSignatures (#1033)
* feat: add logic for verifying ES256 JsonWebSignatures * chore: use google-http-client's Preconditions wrapper * refactor: make DerEncoder an outer class
1 parent ca34202 commit bb4227f

File tree

4 files changed

+124
-12
lines changed

4 files changed

+124
-12
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.api.client.json.webtoken;
18+
19+
import com.google.api.client.util.Preconditions;
20+
21+
import java.math.BigInteger;
22+
import java.util.Arrays;
23+
24+
/**
25+
* Utilities for re-encoding a signature byte array with DER encoding.
26+
*
27+
* <p>Note: that this is not a general purpose encoder and currently only
28+
* handles 512 bit signatures. ES256 verification algorithms expect the
29+
* signature bytes in DER encoding.
30+
*/
31+
public class DerEncoder {
32+
private static byte DER_TAG_SIGNATURE_OBJECT = 0x30;
33+
private static byte DER_TAG_ASN1_INTEGER = 0x02;
34+
35+
static byte[] encode(byte[] signature) {
36+
// expect the signature to be 64 bytes long
37+
Preconditions.checkState(signature.length == 64);
38+
39+
byte[] int1 = new BigInteger(1, Arrays.copyOfRange(signature, 0, 32)).toByteArray();
40+
byte[] int2 = new BigInteger(1, Arrays.copyOfRange(signature, 32, 64)).toByteArray();
41+
byte[] der = new byte[6 + int1.length + int2.length];
42+
43+
// Mark that this is a signature object
44+
der[0] = DER_TAG_SIGNATURE_OBJECT;
45+
der[1] = (byte) (der.length - 2);
46+
47+
// Start ASN1 integer and write the first 32 bits
48+
der[2] = DER_TAG_ASN1_INTEGER;
49+
der[3] = (byte) int1.length;
50+
System.arraycopy(int1, 0, der, 4, int1.length);
51+
52+
// Start ASN1 integer and write the second 32 bits
53+
int offset = int1.length + 4;
54+
der[offset] = DER_TAG_ASN1_INTEGER;
55+
der[offset + 1] = (byte) int2.length;
56+
System.arraycopy(int2, 0, der, offset + 2, int2.length);
57+
58+
return der;
59+
}
60+
}

google-http-client/src/main/java/com/google/api/client/json/webtoken/JsonWebSignature.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.api.client.util.StringUtils;
2424
import java.io.ByteArrayInputStream;
2525
import java.io.IOException;
26+
import java.math.BigInteger;
2627
import java.security.GeneralSecurityException;
2728
import java.security.KeyStore;
2829
import java.security.KeyStoreException;
@@ -349,30 +350,30 @@ public Header getHeader() {
349350
/**
350351
* Verifies the signature of the content.
351352
*
352-
* <p>Currently only {@code "RS256"} algorithm is verified, but others may be added in the future.
353-
* For any other algorithm it returns {@code false}.
353+
* <p>Currently only {@code "RS256"} and {@code "ES256"} algorithms are verified, but others may be added in the
354+
* future. For any other algorithm it returns {@code false}.
354355
*
355356
* @param publicKey public key
356357
* @return whether the algorithm is recognized and it is verified
357358
* @throws GeneralSecurityException
358359
*/
359360
public final boolean verifySignature(PublicKey publicKey) throws GeneralSecurityException {
360-
Signature signatureAlg = null;
361361
String algorithm = getHeader().getAlgorithm();
362362
if ("RS256".equals(algorithm)) {
363-
signatureAlg = SecurityUtils.getSha256WithRsaSignatureAlgorithm();
363+
return SecurityUtils.verify(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), publicKey, signatureBytes, signedContentBytes);
364+
} else if ("ES256".equals(algorithm)) {
365+
return SecurityUtils.verify(SecurityUtils.getEs256SignatureAlgorithm(), publicKey, DerEncoder.encode(signatureBytes), signedContentBytes);
364366
} else {
365367
return false;
366368
}
367-
return SecurityUtils.verify(signatureAlg, publicKey, signatureBytes, signedContentBytes);
368369
}
369370

370371
/**
371372
* {@link Beta} <br>
372373
* Verifies the signature of the content using the certificate chain embedded in the signature.
373374
*
374-
* <p>Currently only {@code "RS256"} algorithm is verified, but others may be added in the future.
375-
* For any other algorithm it returns {@code null}.
375+
* <p>Currently only {@code "RS256"} and {@code "ES256"} algorithms are verified, but others may be added in the
376+
* future. For any other algorithm it returns {@code null}.
376377
*
377378
* <p>The leaf certificate of the certificate chain must be an SSL server certificate.
378379
*
@@ -390,14 +391,13 @@ public final X509Certificate verifySignature(X509TrustManager trustManager)
390391
return null;
391392
}
392393
String algorithm = getHeader().getAlgorithm();
393-
Signature signatureAlg = null;
394394
if ("RS256".equals(algorithm)) {
395-
signatureAlg = SecurityUtils.getSha256WithRsaSignatureAlgorithm();
395+
return SecurityUtils.verify(SecurityUtils.getSha256WithRsaSignatureAlgorithm(), trustManager, x509Certificates, signatureBytes, signedContentBytes);
396+
} else if ("ES256".equals(algorithm)) {
397+
return SecurityUtils.verify(SecurityUtils.getEs256SignatureAlgorithm(), trustManager, x509Certificates, DerEncoder.encode(signatureBytes), signedContentBytes);
396398
} else {
397399
return null;
398400
}
399-
return SecurityUtils.verify(
400-
signatureAlg, trustManager, x509Certificates, signatureBytes, signedContentBytes);
401401
}
402402

403403
/**

google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ public static Signature getSha256WithRsaSignatureAlgorithm() throws NoSuchAlgori
127127
return Signature.getInstance("SHA256withRSA");
128128
}
129129

130+
/** Returns the SHA-256 with ECDSA signature algorithm */
131+
public static Signature getEs256SignatureAlgorithm() throws NoSuchAlgorithmException {
132+
return Signature.getInstance("SHA256withECDSA");
133+
}
134+
130135
/**
131136
* Signs content using a private key.
132137
*
@@ -157,7 +162,7 @@ public static boolean verify(
157162
throws InvalidKeyException, SignatureException {
158163
signatureAlgorithm.initVerify(publicKey);
159164
signatureAlgorithm.update(contentBytes);
160-
// SignatureException may be thrown if we are tring the wrong key.
165+
// SignatureException may be thrown if we are trying the wrong key.
161166
try {
162167
return signatureAlgorithm.verify(signatureBytes);
163168
} catch (SignatureException e) {

google-http-client/src/test/java/com/google/api/client/json/webtoken/JsonWebSignatureTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,27 @@
1919
import com.google.api.client.testing.util.SecurityTestUtils;
2020

2121
import java.io.IOException;
22+
import java.math.BigInteger;
23+
import java.security.AlgorithmParameters;
2224
import java.security.GeneralSecurityException;
25+
import java.security.KeyFactory;
26+
import java.security.NoSuchAlgorithmException;
27+
import java.security.PublicKey;
2328
import java.security.cert.X509Certificate;
2429
import java.security.interfaces.RSAPrivateKey;
30+
import java.security.spec.ECGenParameterSpec;
31+
import java.security.spec.ECParameterSpec;
32+
import java.security.spec.ECPoint;
33+
import java.security.spec.ECPublicKeySpec;
34+
import java.security.spec.InvalidKeySpecException;
35+
import java.security.spec.InvalidParameterSpecException;
2536
import java.util.ArrayList;
2637
import java.util.List;
2738

2839
import javax.net.ssl.X509TrustManager;
2940

41+
import com.google.api.client.util.Base64;
42+
import com.google.api.client.util.StringUtils;
3043
import org.junit.Assert;
3144
import org.junit.Test;
3245

@@ -114,4 +127,38 @@ public void testVerifyX509() throws Exception {
114127
public void testVerifyX509WrongCa() throws Exception {
115128
Assert.assertNull(verifyX509WithCaCert(TestCertificates.BOGUS_CA_CERT));
116129
}
130+
131+
private static final String ES256_CONTENT = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1wZjBEQSJ9.eyJhdWQiOiIvcHJvamVjdHMvNjUyNTYyNzc2Nzk4L2FwcHMvY2xvdWQtc2FtcGxlcy10ZXN0cy1waHAtaWFwIiwiZW1haWwiOiJjaGluZ29yQGdvb2dsZS5jb20iLCJleHAiOjE1ODQwNDc2MTcsImdvb2dsZSI6eyJhY2Nlc3NfbGV2ZWxzIjpbImFjY2Vzc1BvbGljaWVzLzUxODU1MTI4MDkyNC9hY2Nlc3NMZXZlbHMvcmVjZW50U2VjdXJlQ29ubmVjdERhdGEiLCJhY2Nlc3NQb2xpY2llcy81MTg1NTEyODA5MjQvYWNjZXNzTGV2ZWxzL3Rlc3ROb09wIiwiYWNjZXNzUG9saWNpZXMvNTE4NTUxMjgwOTI0L2FjY2Vzc0xldmVscy9ldmFwb3JhdGlvblFhRGF0YUZ1bGx5VHJ1c3RlZCJdfSwiaGQiOiJnb29nbGUuY29tIiwiaWF0IjoxNTg0MDQ3MDE3LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIiwic3ViIjoiYWNjb3VudHMuZ29vZ2xlLmNvbToxMTIxODE3MTI3NzEyMDE5NzI4OTEifQ";
132+
private static final String ES256_SIGNATURE = "yKNtdFY5EKkRboYNexBdfugzLhC3VuGyFcuFYA8kgpxMqfyxa41zkML68hYKrWu2kOBTUW95UnbGpsIi_u1fiA";
133+
134+
// x, y values for keyId "mpf0DA" from https://www.gstatic.com/iap/verify/public_key-jwk
135+
private static final String GOOGLE_ES256_X = "fHEdeT3a6KaC1kbwov73ZwB_SiUHEyKQwUUtMCEn0aI";
136+
private static final String GOOGLE_ES256_Y = "QWOjwPhInNuPlqjxLQyhveXpWqOFcQPhZ3t-koMNbZI";
137+
138+
private PublicKey buildEs256PublicKey(String x, String y)
139+
throws NoSuchAlgorithmException, InvalidParameterSpecException, InvalidKeySpecException {
140+
AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC");
141+
parameters.init(new ECGenParameterSpec("secp256r1"));
142+
ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(
143+
new ECPoint(
144+
new BigInteger(1, Base64.decodeBase64(x)),
145+
new BigInteger(1, Base64.decodeBase64(y))
146+
),
147+
parameters.getParameterSpec(ECParameterSpec.class)
148+
);
149+
KeyFactory keyFactory = KeyFactory.getInstance("EC");
150+
return keyFactory.generatePublic(ecPublicKeySpec);
151+
}
152+
153+
@Test
154+
public void testVerifyES256() throws Exception {
155+
PublicKey publicKey = buildEs256PublicKey(GOOGLE_ES256_X, GOOGLE_ES256_Y);
156+
JsonWebSignature.Header header = new JsonWebSignature.Header();
157+
header.setAlgorithm("ES256");
158+
JsonWebSignature.Payload payload = new JsonWebToken.Payload();
159+
byte[] signatureBytes = Base64.decodeBase64(ES256_SIGNATURE);
160+
byte[] signedContentBytes = StringUtils.getBytesUtf8(ES256_CONTENT);
161+
JsonWebSignature jsonWebSignature = new JsonWebSignature(header, payload, signatureBytes, signedContentBytes);
162+
Assert.assertTrue(jsonWebSignature.verifySignature(publicKey));
163+
}
117164
}

0 commit comments

Comments
 (0)