Skip to content

Commit

Permalink
Merge pull request #207 from DP-3T/bugfix/fix-signature-verification-…
Browse files Browse the repository at this point in the history
…interceptor

make sure SignatureVerificationInterceptor always checks the full body size
  • Loading branch information
simonroesch authored Sep 17, 2020
2 parents 1790f04 + f20db7c commit cc7ecd9
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 10 deletions.
6 changes: 3 additions & 3 deletions dp3t-sdk/sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'

implementation 'io.jsonwebtoken:jjwt-api:0.11.1'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.1'
runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.1') {
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.11.2') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.bouncycastle:bcprov-jdk15on:1.65'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright (c) 2020 Ubique Innovation AG <https://www.ubique.ch>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/
package org.dpppt.android.sdk.internal.backend;

import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;

import java.io.IOException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;

import org.dpppt.android.sdk.backend.SignatureException;
import org.dpppt.android.sdk.internal.logger.LogLevel;
import org.dpppt.android.sdk.internal.logger.Logger;
import org.dpppt.android.sdk.internal.util.Base64Util;
import org.dpppt.android.sdk.models.DayDate;
import org.dpppt.android.sdk.util.SignatureUtil;
import org.junit.Before;
import org.junit.Test;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

import static org.dpppt.android.sdk.util.SignatureUtil.JWS_CLAIM_CONTENT_HASH;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

public class SignatureVerificationInterceptorTest {

Context context;
MockWebServer server;
BackendBucketRepository bucketRepository;
KeyPair keyPair;

@Before
public void setup() {
context = InstrumentationRegistry.getInstrumentation().getContext();

Logger.init(context, LogLevel.DEBUG);

ProxyConfig.DISABLE_SYSTEM_PROXY = true;

server = new MockWebServer();
keyPair = Keys.keyPairFor(SignatureAlgorithm.ES256);

bucketRepository = new BackendBucketRepository(context, server.url("/bucket/").toString(), keyPair.getPublic());
}

private String getJwtForContent(String content) {
HashMap<String, Object> claims = new HashMap<>();
try {
MessageDigest digest = MessageDigest.getInstance(SignatureUtil.HASH_ALGO);
claims.put(JWS_CLAIM_CONTENT_HASH, Base64Util.toBase64(digest.digest(content.getBytes())));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return Jwts.builder().addClaims(claims).signWith(keyPair.getPrivate()).compact();
}

@Test
public void testValidSignature() throws IOException, StatusCodeException {
String responseString = "someRandomContent";
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {

return new MockResponse()
.setResponseCode(200)
.setBody(responseString)
.addHeader(SignatureUtil.HTTP_HEADER_JWS, getJwtForContent(responseString));
}
});
String response = bucketRepository.getGaenExposees(new DayDate(), null).body().string();
assertEquals(responseString, response);
}

@Test
public void testInvalidSignature() throws IOException, StatusCodeException {
String responseString = "someRandomContent";
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
return new MockResponse()
.setResponseCode(200)
.setBody(responseString)
.addHeader(SignatureUtil.HTTP_HEADER_JWS, getJwtForContent("differentContent"));
}
});
try {
bucketRepository.getGaenExposees(new DayDate(), null).body().string();
fail();
} catch (SignatureException e) {
assertEquals("Signature mismatch", e.getMessage());
}
}

@Test
public void testInvalidJwt() throws IOException, StatusCodeException {
String responseString = "someRandomContent";
server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
return new MockResponse()
.setResponseCode(200)
.setBody(responseString)
.addHeader(SignatureUtil.HTTP_HEADER_JWS,
"eyJhbGciOiJFUzI1NiJ9.eyJjb250ZW50LWhhc2giOiJsTzd3TDBkOFl5MFBSaU" +
"w5NGhUa2txMkRXNUxXVjlPNi9zRWNZVDJHZ2t3PSIsImhhc2gtYWxnIjoic2hhLTI1Ni" +
"IsImlzcyI6ImRwM3QiLCJpYXQiOjE1ODgwODk2MDAsImV4cCI6MTU4OTkwNDAwMCwiYm" +
"F0Y2gtcmVsZWFzZS10aW1lIjoiMTU4ODA4OTYwMDAwMCJ9.1uiVGBOWqD8jLKm0_EOmN" +
"MMgHr4FQOsD1ci4iWR1QMitg_MPgtbuggedbuggedbuggedbuggedbuggedbugged");
}
});
try {
bucketRepository.getGaenExposees(new DayDate(), null).body().string();
fail();
} catch (SignatureException e) {
assertEquals("JWT signature does not match locally computed signature. " +
"JWT validity cannot be asserted and should not be trusted.", e.getMessage());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import androidx.annotation.NonNull;

import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
Expand All @@ -21,11 +22,12 @@

import okhttp3.Interceptor;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSource;
import okio.Okio;

public class SignatureVerificationInterceptor implements Interceptor {

private static final long PEEK_MEMORY_LIMIT = 64 * 1024 * 1024L;

private final PublicKey publicKey;

public SignatureVerificationInterceptor(PublicKey publicKey) {
Expand Down Expand Up @@ -56,12 +58,20 @@ public Response intercept(@NonNull Chain chain) throws IOException {

byte[] signedContentHash = SignatureUtil.getVerifiedContentHash(jwsHeader, publicKey);

byte[] body = response.peekBody(PEEK_MEMORY_LIMIT).bytes();
ResponseBody body = response.body();
BufferedSource responseBuffer = Okio.buffer(Okio.source(body.byteStream()));

byte[] actualContentHash;
try {
MessageDigest digest = MessageDigest.getInstance(SignatureUtil.HASH_ALGO);
actualContentHash = digest.digest(body);
try (InputStream bodyStream = responseBuffer.peek().inputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = bodyStream.read(buffer)) != -1) {
digest.update(buffer, 0, len);
}
}
actualContentHash = digest.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Expand All @@ -70,7 +80,12 @@ public Response intercept(@NonNull Chain chain) throws IOException {
throw new SignatureException("Signature mismatch");
}

return response;
return response.newBuilder()
.body(ResponseBody.create(
body.contentType(),
body.contentLength(),
responseBuffer)
).build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*
* SPDX-License-Identifier: MPL-2.0
*/

package org.dpppt.android.sdk.util;

import android.util.Base64;
Expand Down Expand Up @@ -35,7 +34,7 @@ public class SignatureUtil {
public static final String HTTP_HEADER_JWS = "signature";
public static final String HASH_ALGO = "SHA-256";

private static final String JWS_CLAIM_CONTENT_HASH = "content-hash";
public static final String JWS_CLAIM_CONTENT_HASH = "content-hash";

public static PublicKey getPublicKeyFromBase64(String publicKeyBase64)
throws NoSuchAlgorithmException, InvalidKeySpecException {
Expand Down

0 comments on commit cc7ecd9

Please sign in to comment.