Skip to content

Commit

Permalink
feat: add logic for verifying ES256 JsonWebSignatures (#1033)
Browse files Browse the repository at this point in the history
* feat: add logic for verifying ES256 JsonWebSignatures

* chore: use google-http-client's Preconditions wrapper

* refactor: make DerEncoder an outer class
  • Loading branch information
chingor13 authored Apr 24, 2020
1 parent ca34202 commit bb4227f
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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));
}
}

0 comments on commit bb4227f

Please sign in to comment.