Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add logic for verifying ES256 JsonWebSignatures #1033

Merged
merged 3 commits into from Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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;
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -349,30 +350,30 @@ public Header getHeader() {
/**
* 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.
*
Expand All @@ -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);
}

/**
Expand Down
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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) {
Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}
}