-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
10 changed files
with
322 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
FROM openjdk:8-jdk-stretch | ||
|
||
RUN apt-get update --fix-missing | ||
RUN apt-get install -y vim | ||
COPY ./ /var/www | ||
WORKDIR /var/www | ||
EXPOSE 8080 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Google AdMob Rewarded Ads Server Side Verification | ||
|
||
Server-side verification callbacks are URL requests, with query parameters | ||
expanded by Google, that are sent by Google to an external system to notify it | ||
that a user should be rewarded for interacting with a rewarded video ad. | ||
Rewarded video SSV (server-side verification) callbacks provide an extra layer | ||
of protection against spoofing of client-side callbacks to reward users. | ||
|
||
## Description | ||
|
||
This project is developed in Java spring-boot framework as an example to verify | ||
rewarded video SSV callbacks by using the Tink third-party cryptographic library | ||
to ensure that the query parameters in the callback are legitimate values. | ||
|
||
## How to use | ||
|
||
1. Deploy this project on your preferred web service provider. | ||
2. Follow the | ||
[Set up and test server-sideverification](https://support.google.com/admob/answer/9603226) | ||
instructions to create an ad unit and configure/test your server-side | ||
verification endpoint. | ||
|
||
## Local Development | ||
|
||
To start with Java: | ||
|
||
1 `cd RewardedSSVExample` 2 `./gradlew bootRun` | ||
|
||
To start with Docker: | ||
|
||
`docker-compose up --build` | ||
|
||
## Local testing | ||
|
||
To test a signature and message, send a `GET` request to | ||
`localhost:8080/verify?<dataToVerify>&signature=<signature>&key_id=<key_id>`. | ||
|
||
A successful response looks like this: | ||
|
||
``` | ||
{ | ||
"sig": "ME...Z1c", | ||
"payload": "ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins ×tamp=150777823&transaction_id=12...DEF&user_id=1234567", | ||
"key_id": "1268887", | ||
"verified": "true" | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
plugins { | ||
id 'org.springframework.boot' version '2.2.2.RELEASE' | ||
id 'io.spring.dependency-management' version '1.0.8.RELEASE' | ||
id 'java' | ||
} | ||
|
||
group = 'com.example.rewardedssv' | ||
version = '1.0.0' | ||
sourceCompatibility = '1.8' | ||
|
||
repositories { | ||
mavenCentral() | ||
} | ||
|
||
dependencies { | ||
implementation 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' | ||
implementation 'com.google.crypto.tink:tink-android:1.4.0-rc1' | ||
// tag::actuator[] | ||
implementation 'org.springframework.boot:spring-boot-starter-actuator' | ||
// end::actuator[] | ||
implementation 'org.springframework.boot:spring-boot-starter-web' | ||
// tag::tests[] | ||
testImplementation('org.springframework.boot:spring-boot-starter-test') { | ||
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' | ||
} | ||
// end::tests[] | ||
} | ||
|
||
test { | ||
useJUnitPlatform() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
version: '3' | ||
|
||
services: | ||
ssv: | ||
image: ssv | ||
build: | ||
context: . | ||
dockerfile: Dockerfile | ||
container_name: ssv | ||
ports: | ||
- 8080:8080 | ||
volumes: | ||
- .:/var/www | ||
command: ./gradlew bootRun | ||
networks: ['stack'] | ||
networks: | ||
stack: | ||
driver: bridge |
6 changes: 6 additions & 0 deletions
6
java/advanced/RewardedSSVExample/gradle/wrapper/gradle-wrapper.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
#Thu May 14 12:27:25 IST 2020 | ||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip | ||
distributionBase=GRADLE_USER_HOME | ||
distributionPath=wrapper/dists | ||
zipStorePath=wrapper/dists | ||
zipStoreBase=GRADLE_USER_HOME |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-parent</artifactId> | ||
<version>2.2.2.RELEASE</version> | ||
<relativePath/> <!-- lookup parent from repository --> | ||
</parent> | ||
<groupId>com.example</groupId> | ||
<artifactId>rewardedssv</artifactId> | ||
<version>1.0.0</version> | ||
<name>spring-boot</name> | ||
<description>Demo project for Spring Boot</description> | ||
|
||
<properties> | ||
<java.version>1.8</java.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-web</artifactId> | ||
</dependency> | ||
|
||
<!-- tag::actuator[] --> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-actuator</artifactId> | ||
</dependency> | ||
<!-- end::actuator[] --> | ||
|
||
<!-- tag::tests[] --> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter-test</artifactId> | ||
<scope>test</scope> | ||
<exclusions> | ||
<exclusion> | ||
<groupId>org.junit.vintage</groupId> | ||
<artifactId>junit-vintage-engine</artifactId> | ||
</exclusion> | ||
</exclusions> | ||
</dependency> | ||
<!-- end::tests[] --> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-maven-plugin</artifactId> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
|
||
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
rootProject.name = 'rewarded-ssv' |
21 changes: 21 additions & 0 deletions
21
java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/Application.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.example.rewardedssv; | ||
|
||
import org.springframework.boot.CommandLineRunner; | ||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.context.annotation.Bean; | ||
|
||
/** Application entry point */ | ||
@SpringBootApplication | ||
public class Application { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(Application.class, args); | ||
} | ||
|
||
@Bean | ||
public CommandLineRunner commandLineRunner(ApplicationContext ctx) { | ||
return args -> {}; | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/SSVController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package com.example.rewardedssv; | ||
|
||
import com.google.crypto.tink.subtle.Base64; | ||
import com.google.crypto.tink.subtle.EcdsaVerifyJce; | ||
import com.google.crypto.tink.subtle.EllipticCurves; | ||
import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding; | ||
import com.google.crypto.tink.subtle.Enums.HashType; | ||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStreamReader; | ||
import java.net.HttpURLConnection; | ||
import java.net.URL; | ||
import java.nio.charset.Charset; | ||
import java.security.GeneralSecurityException; | ||
import java.security.interfaces.ECPublicKey; | ||
import java.util.Enumeration; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import javax.servlet.http.HttpServletRequest; | ||
import org.json.JSONArray; | ||
import org.json.JSONException; | ||
import org.json.JSONObject; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
/** SSV REST Controller */ | ||
@RestController | ||
public class SSVController { | ||
private static final String SIGNATURE_PARAM_KEY = "signature"; | ||
private static final String KEY_ID_PARAM_KEY = "key_id"; | ||
private static final String REWARD_VERIFIER_KEYS_URL = | ||
"https://www.gstatic.com/admob/reward/verifier-keys.json"; | ||
|
||
private static Map<Long, ECPublicKey> parsePublicKeysJson() | ||
throws GeneralSecurityException, IOException, JSONException { | ||
URL url = new URL(REWARD_VERIFIER_KEYS_URL); | ||
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||
connection.setRequestMethod("GET"); | ||
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); | ||
String inputLine; | ||
StringBuffer content = new StringBuffer(); | ||
while ((inputLine = reader.readLine()) != null) { | ||
content.append(inputLine); | ||
} | ||
reader.close(); | ||
connection.disconnect(); | ||
String publicKeysJson = content.toString(); | ||
JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys"); | ||
Map<Long, ECPublicKey> publicKeys = new HashMap<>(); | ||
for (int i = 0; i < keys.length(); i++) { | ||
JSONObject key = keys.getJSONObject(i); | ||
publicKeys.put( | ||
key.getLong("keyId"), | ||
EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64")))); | ||
} | ||
if (publicKeys.isEmpty()) { | ||
throw new GeneralSecurityException("No trusted keys are available for this protocol version"); | ||
} | ||
return publicKeys; | ||
} | ||
|
||
private void verify(final byte[] dataToVerify, Long keyId, final byte[] signature) | ||
throws GeneralSecurityException { | ||
try { | ||
Map<Long, ECPublicKey> publicKeys = parsePublicKeysJson(); | ||
if (publicKeys.containsKey(keyId)) { | ||
ECPublicKey publicKey = publicKeys.get(keyId); | ||
EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER); | ||
verifier.verify(signature, dataToVerify); | ||
} else { | ||
throw new GeneralSecurityException( | ||
String.format("Cannot find verifying key with key id: %s.", keyId)); | ||
} | ||
} catch (JSONException exception) { | ||
throw new GeneralSecurityException(exception); | ||
} catch (IOException exception) { | ||
throw new GeneralSecurityException(exception); | ||
} | ||
} | ||
|
||
@GetMapping(value = "/verify") | ||
public ResponseEntity<?> index(HttpServletRequest request) { | ||
|
||
Enumeration enumeration = request.getParameterNames(); | ||
Map<String, String[]> parameters = request.getParameterMap(); | ||
|
||
Map<String, String> response = new HashMap<>(); | ||
if (!parameters.containsKey(KEY_ID_PARAM_KEY) || !parameters.containsKey(SIGNATURE_PARAM_KEY)) { | ||
response.put("verified", Boolean.FALSE.toString()); | ||
response.put("error", "Missing key_id and/or signature parameters."); | ||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); | ||
} | ||
|
||
Long keyId = Long.valueOf(parameters.get(KEY_ID_PARAM_KEY)[0]); | ||
String signature = parameters.get(SIGNATURE_PARAM_KEY)[0]; | ||
String queryString = request.getQueryString(); | ||
/* The last two query parameters of rewarded video | ||
SSV callbacks are always signature and key_id | ||
https://developers.google.com/admob/android/rewarded-video-ssv#get_content_to_be_verified | ||
*/ | ||
byte[] payload = | ||
queryString | ||
.substring(0, queryString.indexOf(SIGNATURE_PARAM_KEY) - 1) | ||
.getBytes(Charset.forName("UTF-8")); | ||
|
||
response.put("payload", new String(payload)); | ||
response.put("key_id", keyId.toString()); | ||
response.put("sig", signature); | ||
HttpStatus status = HttpStatus.OK; | ||
try { | ||
verify(payload, keyId, Base64.urlSafeDecode(signature)); | ||
response.put("verified", Boolean.TRUE.toString()); | ||
} catch (GeneralSecurityException exception) { | ||
status = HttpStatus.BAD_REQUEST; | ||
response.put("verified", Boolean.FALSE.toString()); | ||
response.put("error", exception.getMessage()); | ||
} | ||
return new ResponseEntity<>(response, status); | ||
} | ||
} |