Repository: google/exposure-notifications-server
Pull request #299 (2020-05-15 17:30:59)

complete move of transmission risk to per-exposure key
/assign @peetsasaki 
/hold

This will be held until Monday the 18th to allow the android reference app to catch up.

Also - awaiting test data from android team to update failing unit tests.

google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/299#" title="Author self-approved">mikehelmick</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
mikehelmick: /fixes #287
google-prow-robot: @mikehelmick: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-release-unit | f027ff10f856379d7d7ee42ca708a1fe1129c724 | [link](https://prow-internal.gcpnode.com/view/gcs/gke-prow/pr-logs/pull/google_exposure-notifications-server/299/pull-release-unit/1261348417745981441) | `/test pull-release-unit`

[Full PR test history](https://prow-internal.gcpnode.com/pr-history?org=google&repo=exposure-notifications-server&pr=299). [Your PR dashboard](https://gubernator-internal.googleplex.com/pr/mikehelmick).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/299#" title="Author self-approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/299#pullrequestreview-412848539" title="Approved">peetsasaki</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
google-prow-robot: New changes are detected. LGTM label has been removed.
mikehelmick: /cancel hold
mikehelmick: /unhold
mikehelmick: PTAL - this now has valid safetynet payloads from the reference android app
google-prow-robot: @mikehelmick: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pull-release-unit | cf7776df70ad8cdca89adbcbdf46a6cf9b980fa6 | [link](https://prow-internal.gcpnode.com/view/gcs/gke-prow/pr-logs/pull/google_exposure-notifications-server/299/pull-release-unit/1261454114441662466) | `/test pull-release-unit`

[Full PR test history](https://prow-internal.gcpnode.com/pr-history?org=google&repo=exposure-notifications-server&pr=299). [Your PR dashboard](https://gubernator-internal.googleplex.com/pr/mikehelmick).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
google-prow-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/google/exposure-notifications-server/pull/299#pullrequestreview-413029147" title="LGTM">gurayAlsac</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/299#" title="Author self-approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/299#pullrequestreview-412848539" title="Approved">peetsasaki</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=google%2Fexposure-notifications-server).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/google/exposure-notifications-server/blob/master/OWNERS)~~ [mikehelmick]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
google-prow-robot: New changes are detected. LGTM label has been removed.
mikehelmick: one more PTAL - tests passing
gurayAlsac: /lgtm /approve
mikehelmick: /lgtm
google-prow-robot: @mikehelmick: you cannot LGTM your own PR.

<details>

In response to [this](https://github.com/google/exposure-notifications-server/pull/299#issuecomment-629562827):

>/lgtm


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
gurayAlsac: /lgtm

Patch:
diff --git a/docs/server_functional_requirements.md b/docs/server_functional_requirements.md
index 87fb163..cdb7b3e 100644
--- a/docs/server_functional_requirements.md
+++ b/docs/server_functional_requirements.md
@@ -69,6 +69,12 @@ Minimum required fields, followed by a JSON example:
         * Valid values are [1..144]
         * If not present, 144 is the default value (1 day of intervals)
       * Description: Number of intervals that the key is valid for
+    * `transmissionRisk` (**REQUIRED**)
+      * Type: Integer
+      * **The values and meanings of this enum are not finalized at this time.** //TODO(llatif): check status
+      * Constraints:
+        * Valid values range from 0-8
+      * Description: //TODO(llatif): Add description
 * `regions` (**REQUIRED**)
   [ISO 3166 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) format.
   * Type: Array of string
@@ -82,12 +88,6 @@ Minimum required fields, followed by a JSON example:
 * `platform` (**REQUIRED**)
   * Type: string
   * Description: Mobile device platform this request originated from.
-* `transmissionRisk` (**REQUIRED**)
-  * Type: Integer
-  * **The values and meanings of this enum are not finalized at this time.** //TODO(llatif): check status
-  * Constraints:
-    * Valid values range from 0-8
-  * Description: //TODO(llatif): Add description
 * `deviceVerificationPayload` (**REQUIRED**)
   * Type: String
   * Description:  Verification payload.
@@ -109,13 +109,12 @@ The following snippet is an example POST request payload in JSON format.
 ```json
 {
   "temporaryTracingKeys": [
-    {"key": "base64 KEY1", "rollingStartNumber": 12345, "rollingPeriod": 144},
-    {"key": "base64 KEY2", "rollingStartNumber": 12489, "rollingPeriod": 10},
-    {"key": "base64 KEYN", "rollingStartNumber": 12499, "rollingPeriod": 100}],
+    {"key": "base64 KEY1", "rollingStartNumber": 12345, "rollingPeriod": 144, "transmissionRisk": 5},
+    {"key": "base64 KEY2", "rollingStartNumber": 12489, "rollingPeriod": 10, "transmissionRisk": 6},
+    {"key": "base64 KEYN", "rollingStartNumber": 12499, "rollingPeriod": 100, "transmissionRisk": 7}],
   "regions": ["US", "CA", "MX"],
   "appPackageName": "com.foo.app",
   "platform": "android",
-  "diagnosisStatus": 2,
   "deviceVerificationPayload": "base64 encoded attestation payload string",
   "verificationPayload": "signature /code from  of verifying authority",
   "padding": "random string data..."
diff --git a/internal/android/nonce.go b/internal/android/nonce.go
index 75a6fdd..97518d8 100644
--- a/internal/android/nonce.go
+++ b/internal/android/nonce.go
@@ -34,11 +34,10 @@ type Noncer interface {
 var _ Noncer = (*nonceData)(nil)
 
 type nonceData struct {
-	appPackageName   string
-	transmissionRisk int
-	ttKeysBase64     []model.ExposureKey
-	regions          []string
-	verification     string
+	appPackageName string
+	ttKeysBase64   []model.ExposureKey
+	regions        []string
+	verification   string
 }
 
 // NewNonce Creates a new `Noncer{}` based on the inbound publish request.
@@ -60,11 +59,10 @@ func NewNonce(publish *model.Publish) Noncer {
 	sort.Strings(sortedRegions)
 
 	return &nonceData{
-		appPackageName:   publish.AppPackageName,
-		transmissionRisk: publish.TransmissionRisk,
-		ttKeysBase64:     sortedKeys,
-		regions:          sortedRegions,
-		verification:     publish.VerificationPayload,
+		appPackageName: publish.AppPackageName,
+		ttKeysBase64:   sortedKeys,
+		regions:        sortedRegions,
+		verification:   publish.VerificationPayload,
 	}
 }
 
@@ -72,21 +70,20 @@ func NewNonce(publish *model.Publish) Noncer {
 func (n *nonceData) Nonce() string {
 	keys := make([]string, 0, len(n.ttKeysBase64))
 	for _, k := range n.ttKeysBase64 {
-		keys = append(keys, fmt.Sprintf("%v.%v.%v", k.Key, k.IntervalNumber, k.IntervalCount))
+		keys = append(keys, fmt.Sprintf("%v.%v.%v.%v", k.Key, k.IntervalNumber, k.IntervalCount, k.TransmissionRisk))
 	}
 
 	// The cleartext is a combination of all of the data on the request
 	// in a specific order.
 	//
-	// appPackageName|transmissionRisk|key[,key]|region[,region]|verificationAuthorityName
+	// appPackageName|key[,key]|region[,region]|verificationAuthorityName
 	// Keys are ancoded as
-	//     base64(exposureKey).itnervalNumber.IntervalCount
+	//     base64(exposureKey).itnervalNumber.IntervalCount.transmissionRisk
 	// When there is > 1 key, keys are comma separated.
 	// Keys must in sorted order based on the sorting of the base64 exposure key.
 	// Regions are uppercased, sorted, and comma sepreated
 	cleartext :=
 		n.appPackageName + "|" +
-			fmt.Sprintf("%v", n.transmissionRisk) + "|" +
 			strings.Join(keys, ",") + "|" + // where key is b64key.intervalNum.intervalCount
 			strings.Join(n.regions, ",") + "|" +
 			n.verification
diff --git a/internal/model/exposure.go b/internal/model/exposure.go
index 028ba73..2151f29 100644
--- a/internal/model/exposure.go
+++ b/internal/model/exposure.go
@@ -64,7 +64,6 @@ type Publish struct {
 	Regions                   []string      `json:"regions"`
 	AppPackageName            string        `json:"appPackageName"`
 	Platform                  string        `json:"platform"`
-	TransmissionRisk          int           `json:"transmissionRisk"` // DEPRECATED
 	DeviceVerificationPayload string        `json:"deviceVerificationPayload"`
 	VerificationPayload       string        `json:"verificationPayload"`
 	Padding                   string        `json:"padding"`
@@ -174,13 +173,6 @@ func (t *Transformer) TransformPublish(inData *Publish, batchTime time.Time) ([]
 		upcaseRegions[i] = strings.ToUpper(r)
 	}
 
-	// Transmission risk for the whole batch is deprecated - it is now part
-	// of the ExposureKey.
-	defaultTR := inData.TransmissionRisk
-	if defaultTR < MinTransmissionRisk || defaultTR > MaxTransmissionRisk {
-		return nil, fmt.Errorf("invalid transmission risk: %v, must be >= %v && <= %v", defaultTR, MinTransmissionRisk, MaxTransmissionRisk)
-	}
-
 	for _, exposureKey := range inData.Keys {
 		binKey, err := base64util.DecodeString(exposureKey.Key)
 		if err != nil {
@@ -203,18 +195,13 @@ func (t *Transformer) TransformPublish(inData *Publish, batchTime time.Time) ([]
 			return nil, fmt.Errorf("interval number %v is in the future, must be < %v", exposureKey.IntervalNumber, maxIntervalNumber)
 		}
 
-		tr := exposureKey.TransmissionRisk
-		// This is temporary - while we reprecate per-request tranismission risk
-		if tr == 0 && defaultTR != 0 {
-			tr = defaultTR
-		}
-		if tr < MinTransmissionRisk || tr > MaxTransmissionRisk {
+		if tr := exposureKey.TransmissionRisk; tr < MinTransmissionRisk || tr > MaxTransmissionRisk {
 			return nil, fmt.Errorf("invalid transmission risk: %v, must be >= %v && <= %v", tr, MinTransmissionRisk, MaxTransmissionRisk)
 		}
 
 		exposure := &Exposure{
 			ExposureKey:      binKey,
-			TransmissionRisk: tr,
+			TransmissionRisk: exposureKey.TransmissionRisk,
 			AppPackageName:   inData.AppPackageName,
 			Regions:          upcaseRegions,
 			IntervalNumber:   exposureKey.IntervalNumber,


Test patch:
diff --git a/internal/android/nonce_test.go b/internal/android/nonce_test.go
index 9f952fc..aab99f0 100644
--- a/internal/android/nonce_test.go
+++ b/internal/android/nonce_test.go
@@ -29,30 +29,58 @@ func TestGetNonce(t *testing.T) {
 		{
 			Data: model.Publish{
 				Keys: []model.ExposureKey{
-					{Key: "x21Goi8X9m/glOZ0+wz8fA", IntervalNumber: 263123, IntervalCount: 144},
-					{Key: "2mvFSmRsFmJR5r07dxGSjg", IntervalNumber: 263267, IntervalCount: 144},
-					{Key: "6bAd3dv7p+VEuaJVkVItaQ", IntervalNumber: 263411, IntervalCount: 27},
+					{
+						Key:              "x21Goi8X9m/glOZ0+wz8fA",
+						IntervalNumber:   263123,
+						IntervalCount:    144,
+						TransmissionRisk: 6,
+					},
+					{
+						Key:              "2mvFSmRsFmJR5r07dxGSjg",
+						IntervalNumber:   263267,
+						IntervalCount:    144,
+						TransmissionRisk: 7,
+					},
+					{
+						Key:              "6bAd3dv7p+VEuaJVkVItaQ",
+						IntervalNumber:   263411,
+						IntervalCount:    27,
+						TransmissionRisk: 7,
+					},
 				},
 				Regions:             []string{"GB", "US"},
 				AppPackageName:      appPackage,
-				TransmissionRisk:    4,
 				VerificationPayload: "QRTH-ROWO-LOLO-FOOB",
 			},
-			Expected: "xH8QNR09EKuCCuNitam1RgjPaGHO/9p54VikqFdirVY=",
+			Expected: "0kbOC1LIwDc+DQnLOFcIbnxqfKNg58b7WA3vREdd7ZQ=",
 		},
 		{
 			Data: model.Publish{
 				Keys: []model.ExposureKey{
-					{Key: "zdCW5HrOKbirxmQVc0L/eA", IntervalNumber: 263123, IntervalCount: 144},
-					{Key: "t+k51ifogJo9jq3GH9LWGQ", IntervalNumber: 263267, IntervalCount: 144},
-					{Key: "3uXRrSlcv1+OMI3oFtdaUw", IntervalNumber: 263411, IntervalCount: 27},
+					{
+						Key:              "zdCW5HrOKbirxmQVc0L/eA",
+						IntervalNumber:   263123,
+						IntervalCount:    144,
+						TransmissionRisk: 8,
+					},
+					{
+						Key:              "t+k51ifogJo9jq3GH9LWGQ",
+						IntervalNumber:   263267,
+						IntervalCount:    144,
+						TransmissionRisk: 8,
+					},
+					{
+						Key:              "3uXRrSlcv1+OMI3oFtdaUw",
+						IntervalNumber:   263411,
+						IntervalCount:    27,
+						TransmissionRisk: 8,
+					},
 				},
 				Regions:             []string{"gB", "us"},
 				AppPackageName:      appPackage,
-				TransmissionRisk:    7,
 				VerificationPayload: "BREA-KMEO-FFAP-IECE",
 			},
-			Expected: "LHSwWAjTf3nMVTk7LBwMx9Wg7jEPRjEJf1zRtoxQI64=",
+			Expected: "KYCIywFRTwXCnYCF4TTFaH//iMb/fBQENqIhtzrJPxA=",
 		},
 	}
 
diff --git a/internal/android/safetynet_test.go b/internal/android/safetynet_test.go
index f687518..ac35e0c 100644
--- a/internal/android/safetynet_test.go
+++ b/internal/android/safetynet_test.go
@@ -41,94 +41,16 @@ var (
 	// this particular payload is fixed in time (in the past) doesn't pass
 	// basic integrity checks and couldn't be used in a real system.
 	// **This comment applies to all test data in this file.**
-	payload = `eyJhbGciOiJSUzI1NiIsIng1YyI6WyJNSUlGa3pDQ0JIdWdBd0lCQWdJUkFOY1Nr` +
-		`amRzNW42K0NBQUFBQUFwYTBjd0RRWUpLb1pJaHZjTkFRRUxCUUF3UWpFTE1Ba0dBMVVFQmhN` +
-		`Q1ZWTXhIakFjQmdOVkJBb1RGVWR2YjJkc1pTQlVjblZ6ZENCVFpYSjJhV05sY3pFVE1CRUdB` +
-		`MVVFQXhNS1IxUlRJRU5CSURGUE1UQWVGdzB5TURBeE1UTXhNVFF4TkRsYUZ3MHlNVEF4TVRF` +
-		`eE1UUXhORGxhTUd3eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXli` +
-		`bWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUk13RVFZRFZRUUtFd3BIYjI5` +
-		`bmJHVWdURXhETVJzd0dRWURWUVFERXhKaGRIUmxjM1F1WVc1a2NtOXBaQzVqYjIwd2dnRWlN` +
-		`QTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNXRXJCUVRHWkdOMWlaYk45` +
-		`ZWhSZ2lmV0J4cWkyUGRneHcwM1A3VHlKWmZNeGpwNUw3ajFHTmVQSzVIemRyVW9JZDF5Q0l5` +
-		`Qk15eHFnYXpxZ3RwWDVXcHNYVzRWZk1oSmJOMVkwOXF6cXA2SkQrMlBaZG9UVTFrRlJBTVdm` +
-		`TC9VdVp0azdwbVJYZ0dtNWpLRHJaOU54ZTA0dk1ZUXI4OE5xd1cva2ZaMWdUT05JVVQwV3NM` +
-		`VC80NTIyQlJXeGZ3eGMzUUUxK1RLV2tMQ3J2ZWs2V2xJcXlhQzUyVzdNRFI4TXBGZWJ5bVNL` +
-		`VHZ3Zk1Sd3lLUUxUMDNVTDR2dDQ4eUVjOHNwN3dUQUhNL1dEZzhRb3RhcmY4T0JIa25vWjky` +
-		`WGl2aWFWNnRRcWhST0hDZmdtbkNYaXhmVzB3RVhDdnFpTFRiUXRVYkxzUy84SVJ0ZFhrcFFC` +
-		`OUFnTUJBQUdqZ2dKWU1JSUNWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dD` +
-		`Z1lJS3dZQkJRVUhBd0V3REFZRFZSMFRBUUgvQkFJd0FEQWRCZ05WSFE0RUZnUVU2REhCd3NB` +
-		`dmI1M2cvQzA3cHJUdnZ3TlFRTFl3SHdZRFZSMGpCQmd3Rm9BVW1OSDRiaERyejV2c1lKOFlr` +
-		`QnVnNjMwSi9Tc3daQVlJS3dZQkJRVUhBUUVFV0RCV01DY0dDQ3NHQVFVRkJ6QUJoaHRvZEhS` +
-		`d09pOHZiMk56Y0M1d2Eya3VaMjl2Wnk5bmRITXhiekV3S3dZSUt3WUJCUVVITUFLR0gyaDBk` +
-		`SEE2THk5d2Eya3VaMjl2Wnk5bmMzSXlMMGRVVXpGUE1TNWpjblF3SFFZRFZSMFJCQll3RklJ` +
-		`U1lYUjBaWE4wTG1GdVpISnZhV1F1WTI5dE1DRUdBMVVkSUFRYU1CZ3dDQVlHWjRFTUFRSUNN` +
-		`QXdHQ2lzR0FRUUIxbmtDQlFNd0x3WURWUjBmQkNnd0pqQWtvQ0tnSUlZZWFIUjBjRG92TDJO` +
-		`eWJDNXdhMmt1WjI5dlp5OUhWRk14VHpFdVkzSnNNSUlCQkFZS0t3WUJCQUhXZVFJRUFnU0I5` +
-		`UVNCOGdEd0FIY0E5bHlVTDlGM01DSVVWQmdJTUpSV2p1Tk5FeGt6djk4TUx5QUx6RTd4Wk9N` +
-		`QUFBRnZudXkwWndBQUJBTUFTREJHQWlFQTdlLzBZUnUzd0FGbVdIMjdNMnZiVmNaL21ycCs0` +
-		`cmZZYy81SVBKMjlGNmdDSVFDbktDQ0FhY1ZOZVlaOENDZllkR3BCMkdzSHh1TU9Ia2EvTzQx` +
-		`aldlRit6Z0IxQUVTVVpTNnc3czZ2eEVBSDJLaitLTURhNW9LKzJNc3h0VC9UTTVhMXRvR29B` +
-		`QUFCYjU3c3RKTUFBQVFEQUVZd1JBSWdFWGJpb1BiSnA5cUMwRGoyNThERkdTUk1BVStaQjFF` +
-		`aVZFYmJiLzRVdk5FQ0lCaEhrQnQxOHZSbjl6RHZ5cmZ4eXVkY0hUT1NsM2dUYVlBLzd5VC9C` +
-		`aUg0TUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFESUFjUUJsbWQ4TUVnTGRycnJNYkJUQ3Zw` +
-		`TVhzdDUrd3gyRGxmYWpKTkpVUDRqWUZqWVVROUIzWDRFMnpmNDluWDNBeXVaRnhBcU9SbmJq` +
-		`LzVqa1k3YThxTUowajE5ekZPQitxZXJ4ZWMwbmhtOGdZbExiUW02c0tZN1AwZXhmcjdIdUsz` +
-		`TWtQMXBlYzE0d0ZFVWFHcUR3VWJHZ2wvb2l6MzhGWENFK0NXOEUxUUFFVWZ2YlFQVFliS3hZ` +
-		`ait0Q05sc3MwYlRTb0wyWjJkL2ozQnBMM01GdzB5eFNLL1VUcXlrTHIyQS9NZGhKUW14aStH` +
-		`K01LUlNzUXI2MkFuWmF1OXE2WUZvaSs5QUVIK0E0OFh0SXlzaEx5Q1RVM0h0K2FLb2hHbnhB` +
-		`NXVsMVhSbXFwOEh2Y0F0MzlQOTVGWkdGSmUwdXZseWpPd0F6WHVNdTdNK1BXUmMiLCJNSUlF` +
-		`U2pDQ0F6S2dBd0lCQWdJTkFlTzBtcUdOaXFtQkpXbFF1REFOQmdrcWhraUc5dzBCQVFzRkFE` +
-		`Qk1NU0F3SGdZRFZRUUxFeGRIYkc5aVlXeFRhV2R1SUZKdmIzUWdRMEVnTFNCU01qRVRNQkVH` +
-		`QTFVRUNoTUtSMnh2WW1Gc1UybG5iakVUTUJFR0ExVUVBeE1LUjJ4dlltRnNVMmxuYmpBZUZ3` +
-		`MHhOekEyTVRVd01EQXdOREphRncweU1URXlNVFV3TURBd05ESmFNRUl4Q3pBSkJnTlZCQVlU` +
-		`QWxWVE1SNHdIQVlEVlFRS0V4VkhiMjluYkdVZ1ZISjFjM1FnVTJWeWRtbGpaWE14RXpBUkJn` +
-		`TlZCQU1UQ2tkVVV5QkRRU0F4VHpFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3` +
-		`Z2dFS0FvSUJBUURRR005RjFJdk4wNXprUU85K3ROMXBJUnZKenp5T1RIVzVEekVaaEQyZVBD` +
-		`bnZVQTBRazI4RmdJQ2ZLcUM5RWtzQzRUMmZXQllrL2pDZkMzUjNWWk1kUy9kTjRaS0NFUFpS` +
-		`ckF6RHNpS1VEelJybUJCSjV3dWRnem5kSU1ZY0xlL1JHR0ZsNXlPRElLZ2pFdi9TSkgvVUwr` +
-		`ZEVhbHROMTFCbXNLK2VRbU1GKytBY3hHTmhyNTlxTS85aWw3MUkyZE44RkdmY2Rkd3VhZWo0` +
-		`YlhocDBMY1FCYmp4TWNJN0pQMGFNM1Q0SStEc2F4bUtGc2JqemFUTkM5dXpwRmxnT0lnN3JS` +
-		`MjV4b3luVXh2OHZObWtxN3pkUEdIWGt4V1k3b0c5aitKa1J5QkFCazdYckpmb3VjQlpFcUZK` +
-		`SlNQazdYQTBMS1cwWTN6NW96MkQwYzF0Skt3SEFnTUJBQUdqZ2dFek1JSUJMekFPQmdOVkhR` +
-		`OEJBZjhFQkFNQ0FZWXdIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01D` +
-		`TUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0hRWURWUjBPQkJZRUZKalIrRzRRNjgrYjdH` +
-		`Q2ZHSkFib090OUNmMHJNQjhHQTFVZEl3UVlNQmFBRkp2aUIxZG5IQjdBYWdiZVdiU2FMZC9j` +
-		`R1lZdU1EVUdDQ3NHQVFVRkJ3RUJCQ2t3SnpBbEJnZ3JCZ0VGQlFjd0FZWVphSFIwY0Rvdkwy` +
-		`OWpjM0F1Y0d0cExtZHZiMmN2WjNOeU1qQXlCZ05WSFI4RUt6QXBNQ2VnSmFBamhpRm9kSFJ3` +
-		`T2k4dlkzSnNMbkJyYVM1bmIyOW5MMmR6Y2pJdlozTnlNaTVqY213d1B3WURWUjBnQkRnd05q` +
-		`QTBCZ1puZ1F3QkFnSXdLakFvQmdnckJnRUZCUWNDQVJZY2FIUjBjSE02THk5d2Eya3VaMjl2` +
-		`Wnk5eVpYQnZjMmwwYjNKNUx6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFHb0ErTm5uNzh5` +
-		`NnBSamQ5WGxRV05hN0hUZ2laL3IzUk5Ha21VbVlIUFFxNlNjdGk5UEVhanZ3UlQyaVdUSFFy` +
-		`MDJmZXNxT3FCWTJFVFV3Z1pRK2xsdG9ORnZoc085dHZCQ09JYXpwc3dXQzlhSjl4anU0dFdE` +
-		`UUg4TlZVNllaWi9YdGVEU0dVOVl6SnFQalk4cTNNRHhyem1xZXBCQ2Y1bzhtdy93SjRhMkc2` +
-		`eHpVcjZGYjZUOE1jRE8yMlBMUkw2dTNNNFR6czNBMk0xajZieWtKWWk4d1dJUmRBdktMV1p1` +
-		`L2F4QlZielltcW13a201ekxTRFc1bklBSmJFTENRQ1p3TUg1NnQyRHZxb2Z4czZCQmNDRkla` +
-		`VVNweHU2eDZ0ZDBWN1N2SkNDb3NpclNtSWF0ai85ZFNTVkRRaWJldDhxLzdVSzR2NFpVTjgw` +
-		`YXRuWnoxeWc9PSJdfQ.eyJub25jZSI6ImRuUmhhR1p6VEhSWlJFVkpiWE5wYm1WeVdsSnhhe` +
-		`XR3T0ZCMVdHWnZlamhvYlZCRGMyaHNVM3BuZHowPSIsInRpbWVzdGFtcE1zIjoxNTg5MTU0M` +
-		`DA2NDk1LCJhcGtQYWNrYWdlTmFtZSI6ImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLmV4cG9zd` +
-		`XJlbm90aWZpY2F0aW9uIiwiYXBrRGlnZXN0U2hhMjU2IjoiVFlLdXRVT3hVekpjZmxFb0x3M` +
-		`y9Ya2xoNXFoK08wanF4WTU2c3YycEZVYz0iLCJjdHNQcm9maWxlTWF0Y2giOnRydWUsImFwa` +
-		`0NlcnRpZmljYXRlRGlnZXN0U2hhMjU2IjpbImpxbVlFcWk5cVV2cFVlMTFxTWYzdjJvNlZFU` +
-		`U0rNU5EZWUyYnoweGR6V2M9Il0sImJhc2ljSW50ZWdyaXR5Ijp0cnVlLCJldmFsdWF0aW9uV` +
-		`HlwZSI6IkJBU0lDIn0.TDOJdtDgDybKwzmvEWQuKxiiktBSPrlF0UrbtBWEovpCa_36orW5g` +
-		`MqmB6_nwviC1PK65dNoXpmbQ4mJa9YwN3Tm99DWNdawbswVRP37yYSQuL2-m_NPfxeXy9qH3` +
-		`8nfyrXwWceZbrjOAxOKZ-NZqpACiahy0PfGezLr8WXGeOT_a41xtP_pjVvG4fblcFso_3kFI` +
-		`F0t9L2uA5W-wZwNUiS_tl0kpbU-wJMHWwyW_aM_6Oyb0e5I1HaLh8v2JLsoFgr9oX0uiAifg` +
-		`m41mNJdsXNHg4k6JZeiFSQQvSq30TJ_VjmYz8hioeDBXQPb3w9aoIwq3_yCq297lJ-r8oMcLQ`
+	payload = `eyJhbGciOiJSUzI1NiIsIng1YyI6WyJNSUlGa3pDQ0JIdWdBd0lCQWdJUkFOY1NramRzNW42K0NBQUFBQUFwYTBjd0RRWUpLb1pJaHZjTkFRRUxCUUF3UWpFTE1Ba0dBMVVFQmhNQ1ZWTXhIakFjQmdOVkJBb1RGVWR2YjJkc1pTQlVjblZ6ZENCVFpYSjJhV05sY3pFVE1CRUdBMVVFQXhNS1IxUlRJRU5CSURGUE1UQWVGdzB5TURBeE1UTXhNVFF4TkRsYUZ3MHlNVEF4TVRFeE1UUXhORGxhTUd3eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxTmIzVnVkR0ZwYmlCV2FXVjNNUk13RVFZRFZRUUtFd3BIYjI5bmJHVWdURXhETVJzd0dRWURWUVFERXhKaGRIUmxjM1F1WVc1a2NtOXBaQzVqYjIwd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUUNXRXJCUVRHWkdOMWlaYk45ZWhSZ2lmV0J4cWkyUGRneHcwM1A3VHlKWmZNeGpwNUw3ajFHTmVQSzVIemRyVW9JZDF5Q0l5Qk15eHFnYXpxZ3RwWDVXcHNYVzRWZk1oSmJOMVkwOXF6cXA2SkQrMlBaZG9UVTFrRlJBTVdmTC9VdVp0azdwbVJYZ0dtNWpLRHJaOU54ZTA0dk1ZUXI4OE5xd1cva2ZaMWdUT05JVVQwV3NMVC80NTIyQlJXeGZ3eGMzUUUxK1RLV2tMQ3J2ZWs2V2xJcXlhQzUyVzdNRFI4TXBGZWJ5bVNLVHZ3Zk1Sd3lLUUxUMDNVTDR2dDQ4eUVjOHNwN3dUQUhNL1dEZzhRb3RhcmY4T0JIa25vWjkyWGl2aWFWNnRRcWhST0hDZmdtbkNYaXhmVzB3RVhDdnFpTFRiUXRVYkxzUy84SVJ0ZFhrcFFCOUFnTUJBQUdqZ2dKWU1JSUNWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUhBd0V3REFZRFZSMFRBUUgvQkFJd0FEQWRCZ05WSFE0RUZnUVU2REhCd3NBdmI1M2cvQzA3cHJUdnZ3TlFRTFl3SHdZRFZSMGpCQmd3Rm9BVW1OSDRiaERyejV2c1lKOFlrQnVnNjMwSi9Tc3daQVlJS3dZQkJRVUhBUUVFV0RCV01DY0dDQ3NHQVFVRkJ6QUJoaHRvZEhSd09pOHZiMk56Y0M1d2Eya3VaMjl2Wnk5bmRITXhiekV3S3dZSUt3WUJCUVVITUFLR0gyaDBkSEE2THk5d2Eya3VaMjl2Wnk5bmMzSXlMMGRVVXpGUE1TNWpjblF3SFFZRFZSMFJCQll3RklJU1lYUjBaWE4wTG1GdVpISnZhV1F1WTI5dE1DRUdBMVVkSUFRYU1CZ3dDQVlHWjRFTUFRSUNNQXdHQ2lzR0FRUUIxbmtDQlFNd0x3WURWUjBmQkNnd0pqQWtvQ0tnSUlZZWFIUjBjRG92TDJOeWJDNXdhMmt1WjI5dlp5OUhWRk14VHpFdVkzSnNNSUlCQkFZS0t3WUJCQUhXZVFJRUFnU0I5UVNCOGdEd0FIY0E5bHlVTDlGM01DSVVWQmdJTUpSV2p1Tk5FeGt6djk4TUx5QUx6RTd4Wk9NQUFBRnZudXkwWndBQUJBTUFTREJHQWlFQTdlLzBZUnUzd0FGbVdIMjdNMnZiVmNaL21ycCs0cmZZYy81SVBKMjlGNmdDSVFDbktDQ0FhY1ZOZVlaOENDZllkR3BCMkdzSHh1TU9Ia2EvTzQxaldlRit6Z0IxQUVTVVpTNnc3czZ2eEVBSDJLaitLTURhNW9LKzJNc3h0VC9UTTVhMXRvR29BQUFCYjU3c3RKTUFBQVFEQUVZd1JBSWdFWGJpb1BiSnA5cUMwRGoyNThERkdTUk1BVStaQjFFaVZFYmJiLzRVdk5FQ0lCaEhrQnQxOHZSbjl6RHZ5cmZ4eXVkY0hUT1NsM2dUYVlBLzd5VC9CaUg0TUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFESUFjUUJsbWQ4TUVnTGRycnJNYkJUQ3ZwTVhzdDUrd3gyRGxmYWpKTkpVUDRqWUZqWVVROUIzWDRFMnpmNDluWDNBeXVaRnhBcU9SbmJqLzVqa1k3YThxTUowajE5ekZPQitxZXJ4ZWMwbmhtOGdZbExiUW02c0tZN1AwZXhmcjdIdUszTWtQMXBlYzE0d0ZFVWFHcUR3VWJHZ2wvb2l6MzhGWENFK0NXOEUxUUFFVWZ2YlFQVFliS3hZait0Q05sc3MwYlRTb0wyWjJkL2ozQnBMM01GdzB5eFNLL1VUcXlrTHIyQS9NZGhKUW14aStHK01LUlNzUXI2MkFuWmF1OXE2WUZvaSs5QUVIK0E0OFh0SXlzaEx5Q1RVM0h0K2FLb2hHbnhBNXVsMVhSbXFwOEh2Y0F0MzlQOTVGWkdGSmUwdXZseWpPd0F6WHVNdTdNK1BXUmMiLCJNSUlFU2pDQ0F6S2dBd0lCQWdJTkFlTzBtcUdOaXFtQkpXbFF1REFOQmdrcWhraUc5dzBCQVFzRkFEQk1NU0F3SGdZRFZRUUxFeGRIYkc5aVlXeFRhV2R1SUZKdmIzUWdRMEVnTFNCU01qRVRNQkVHQTFVRUNoTUtSMnh2WW1Gc1UybG5iakVUTUJFR0ExVUVBeE1LUjJ4dlltRnNVMmxuYmpBZUZ3MHhOekEyTVRVd01EQXdOREphRncweU1URXlNVFV3TURBd05ESmFNRUl4Q3pBSkJnTlZCQVlUQWxWVE1SNHdIQVlEVlFRS0V4VkhiMjluYkdVZ1ZISjFjM1FnVTJWeWRtbGpaWE14RXpBUkJnTlZCQU1UQ2tkVVV5QkRRU0F4VHpFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURRR005RjFJdk4wNXprUU85K3ROMXBJUnZKenp5T1RIVzVEekVaaEQyZVBDbnZVQTBRazI4RmdJQ2ZLcUM5RWtzQzRUMmZXQllrL2pDZkMzUjNWWk1kUy9kTjRaS0NFUFpSckF6RHNpS1VEelJybUJCSjV3dWRnem5kSU1ZY0xlL1JHR0ZsNXlPRElLZ2pFdi9TSkgvVUwrZEVhbHROMTFCbXNLK2VRbU1GKytBY3hHTmhyNTlxTS85aWw3MUkyZE44RkdmY2Rkd3VhZWo0YlhocDBMY1FCYmp4TWNJN0pQMGFNM1Q0SStEc2F4bUtGc2JqemFUTkM5dXpwRmxnT0lnN3JSMjV4b3luVXh2OHZObWtxN3pkUEdIWGt4V1k3b0c5aitKa1J5QkFCazdYckpmb3VjQlpFcUZKSlNQazdYQTBMS1cwWTN6NW96MkQwYzF0Skt3SEFnTUJBQUdqZ2dFek1JSUJMekFPQmdOVkhROEJBZjhFQkFNQ0FZWXdIUVlEVlIwbEJCWXdGQVlJS3dZQkJRVUhBd0VHQ0NzR0FRVUZCd01DTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0hRWURWUjBPQkJZRUZKalIrRzRRNjgrYjdHQ2ZHSkFib090OUNmMHJNQjhHQTFVZEl3UVlNQmFBRkp2aUIxZG5IQjdBYWdiZVdiU2FMZC9jR1lZdU1EVUdDQ3NHQVFVRkJ3RUJCQ2t3SnpBbEJnZ3JCZ0VGQlFjd0FZWVphSFIwY0RvdkwyOWpjM0F1Y0d0cExtZHZiMmN2WjNOeU1qQXlCZ05WSFI4RUt6QXBNQ2VnSmFBamhpRm9kSFJ3T2k4dlkzSnNMbkJyYVM1bmIyOW5MMmR6Y2pJdlozTnlNaTVqY213d1B3WURWUjBnQkRnd05qQTBCZ1puZ1F3QkFnSXdLakFvQmdnckJnRUZCUWNDQVJZY2FIUjBjSE02THk5d2Eya3VaMjl2Wnk5eVpYQnZjMmwwYjNKNUx6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFHb0ErTm5uNzh5NnBSamQ5WGxRV05hN0hUZ2laL3IzUk5Ha21VbVlIUFFxNlNjdGk5UEVhanZ3UlQyaVdUSFFyMDJmZXNxT3FCWTJFVFV3Z1pRK2xsdG9ORnZoc085dHZCQ09JYXpwc3dXQzlhSjl4anU0dFdEUUg4TlZVNllaWi9YdGVEU0dVOVl6SnFQalk4cTNNRHhyem1xZXBCQ2Y1bzhtdy93SjRhMkc2eHpVcjZGYjZUOE1jRE8yMlBMUkw2dTNNNFR6czNBMk0xajZieWtKWWk4d1dJUmRBdktMV1p1L2F4QlZielltcW13a201ekxTRFc1bklBSmJFTENRQ1p3TUg1NnQyRHZxb2Z4czZCQmNDRklaVVNweHU2eDZ0ZDBWN1N2SkNDb3NpclNtSWF0ai85ZFNTVkRRaWJldDhxLzdVSzR2NFpVTjgwYXRuWnoxeWc9PSJdfQ.eyJub25jZSI6IlZreEphblUwYW5CcFkydDNObEZFZVM5d2IzRk9iRlprTHpCaFNYTktiRTVqYkVGSmFESnFRbm94VFQwPSIsInRpbWVzdGFtcE1zIjoxNTg5NTg4NDA4MTg1LCJhcGtQYWNrYWdlTmFtZSI6ImNvbS5nb29nbGUuYW5kcm9pZC5hcHBzLmV4cG9zdXJlbm90aWZpY2F0aW9uIiwiYXBrRGlnZXN0U2hhMjU2IjoiYWt2Y3oxdWgyak85NzF5blZ1bXdsSEM4L2xHZG5vYjVGVlNIWVlabCtWcz0iLCJjdHNQcm9maWxlTWF0Y2giOnRydWUsImFwa0NlcnRpZmljYXRlRGlnZXN0U2hhMjU2IjpbImpxbVlFcWk5cVV2cFVlMTFxTWYzdjJvNlZFUU0rNU5EZWUyYnoweGR6V2M9Il0sImJhc2ljSW50ZWdyaXR5Ijp0cnVlLCJldmFsdWF0aW9uVHlwZSI6IkJBU0lDIn0.cU97Z9Fax39ehQvKpa8C0I0hXIh1zclZErn5uDGdtWsVTddpf1qxSVnAwxAGhBmqbOF_YL-75dGojxT8dsGpe7LBXQr9qyRm79FjfIAAqDQrblvOxIatGbR1qkmLJITF7DAc4sAcK8CP2RxIrAkUIAUAdgwsx4JbXeNMfPxR-XODMo93MaMRLV5byA1MDaRw3RA-HTx81LEAuQJfUcoRf-O7WTnBBYDUDlUAY5_VNp7wGRKg9eNNzbbysPqpS-o-sW67dvsalav2ENwwQT9RJwWR4YCI4bDh_qH3Mz8FfChwefS1AW4nOPsOJwkEhLHIO521ZQiwZVct1b6glPc08g`
 
 	publish = &model.Publish{
 		Keys: []model.ExposureKey{
-			{Key: "HKXVlIO+vDmQNJ2M1MVtHQ==", IntervalNumber: 2647872, IntervalCount: 144},
-			{Key: "JjEtCT9Lcyw5oPiaNcWC/Q==", IntervalNumber: 2648016, IntervalCount: 144},
-			{Key: "cLTwDu9onEv/N6FMV3Uy4Q==", IntervalNumber: 2648160, IntervalCount: 144},
-			{Key: "ko6TsgPP8Wvu+ijpSLbY3A==", IntervalNumber: 2648304, IntervalCount: 144},
-			{Key: "9kMgBy7qdG3o6eh3vAD3mQ==", IntervalNumber: 2648448, IntervalCount: 144},
+			{Key: "QLIvVheW9p6JiTx4pslesg==", IntervalNumber: 2649312, IntervalCount: 144, TransmissionRisk: 1},
+			{Key: "UW7pkDKfLbfLs8LveFyY3w==", IntervalNumber: 2649168, IntervalCount: 144, TransmissionRisk: 1},
 		},
-		TransmissionRisk:    1,
 		AppPackageName:      appPackage,
-		VerificationPayload: "PUBLIC_HEALTH_AUTHORITY",
-		Regions:             []string{"GB"},
+		VerificationPayload: "POSITIVE_TEST_123456",
+		Regions:             []string{"US"},
 	}
 )
 
@@ -154,7 +76,7 @@ func TestValidateAttestation(t *testing.T) {
 	nonce := NewNonce(publish)
 
 	apkDigest := "jqmYEqi9qUvpUe11qMf3v2o6VEQM+5NDee2bz0xdzWc="
-	generateTimeS := int64(1589154006495) / 1000
+	generateTimeS := int64(1589588408185) / 1000
 	generateTime := time.Unix(generateTimeS, 0)
 	maxValidTime := generateTime.Add(1 * time.Minute)
 	minValidTime := generateTime.Add(-1 * time.Minute)
@@ -253,7 +175,7 @@ func TestValidateAttestation(t *testing.T) {
 				MaxValidTime:    maxValidTime,
 			},
 			false,
-			"attestation is too old, must be newer than 1589154066, was 1589154006",
+			"attestation is too old, must be newer than 1589588468, was 1589588408",
 		},
 		{
 			VerifyOpts{
@@ -266,7 +188,7 @@ func TestValidateAttestation(t *testing.T) {
 				MaxValidTime:    minValidTime,
 			},
 			false,
-			"attestation is in the future, must be older than 1589153946, was 1589154006",
+			"attestation is in the future, must be older than 1589588348, was 1589588408",
 		},
 	}
 
diff --git a/internal/api/jsonutil/unmarshal_test.go b/internal/api/jsonutil/unmarshal_test.go
index b9da711..3a0ac82 100644
--- a/internal/api/jsonutil/unmarshal_test.go
+++ b/internal/api/jsonutil/unmarshal_test.go
@@ -115,13 +115,12 @@ func TestInvalidStructure(t *testing.T) {
 func TestValidPublishMessage(t *testing.T) {
 	intervalNumber := int32(time.Date(2020, 04, 17, 20, 04, 01, 1, time.UTC).Unix() / 600)
 	json := `{"temporaryExposureKeys": [
-		  {"key": "ABC", "rollingStartNumber": %v, "rollingPeriod": 144},
-		  {"key": "DEF", "rollingStartNumber": %v, "rollingPeriod": 122},
-			{"key": "123", "rollingStartNumber": %v, "rollingPeriod": 1}],
+		  {"key": "ABC", "rollingStartNumber": %v, "rollingPeriod": 144, "TransmissionRisk": 2},
+		  {"key": "DEF", "rollingStartNumber": %v, "rollingPeriod": 122, "TransmissionRisk": 2},
+			{"key": "123", "rollingStartNumber": %v, "rollingPeriod": 1, "TransmissionRisk": 2}],
     "appPackageName": "com.google.android.awesome",
     "platform": "android",
     "regions": ["CA", "US"],
-		"transmissionRisk": 2,
     "DeviceVerificationPayload": "foo",
     "VerificationPayload": "1234-ABCD-EFGH-5678"}`
 	json = fmt.Sprintf(json, intervalNumber, intervalNumber, intervalNumber)
@@ -143,14 +142,13 @@ func TestValidPublishMessage(t *testing.T) {
 
 	want := &model.Publish{
 		Keys: []model.ExposureKey{
-			{Key: "ABC", IntervalNumber: intervalNumber, IntervalCount: 144},
-			{Key: "DEF", IntervalNumber: intervalNumber, IntervalCount: 122},
-			{Key: "123", IntervalNumber: intervalNumber, IntervalCount: 1},
+			{Key: "ABC", IntervalNumber: intervalNumber, IntervalCount: 144, TransmissionRisk: 2},
+			{Key: "DEF", IntervalNumber: intervalNumber, IntervalCount: 122, TransmissionRisk: 2},
+			{Key: "123", IntervalNumber: intervalNumber, IntervalCount: 1, TransmissionRisk: 2},
 		},
 		Regions:                   []string{"CA", "US"},
 		Platform:                  "android",
 		AppPackageName:            "com.google.android.awesome",
-		TransmissionRisk:          2,
 		DeviceVerificationPayload: "foo",
 		VerificationPayload:       "1234-ABCD-EFGH-5678",
 	}
diff --git a/internal/model/exposure_test.go b/internal/model/exposure_test.go
index c069a1b..7341659 100644
--- a/internal/model/exposure_test.go
+++ b/internal/model/exposure_test.go
@@ -170,7 +170,6 @@ func TestPublishValidation(t *testing.T) {
 				Keys: []ExposureKey{
 					{Key: encodeKey(generateKey(t)[0 : KeyLength-2])},
 				},
-				TransmissionRisk: MaxTransmissionRisk - 1,
 			},
 			m: fmt.Sprintf("invalid key length, %v, must be %v", KeyLength-2, KeyLength),
 		},
@@ -183,7 +182,6 @@ func TestPublishValidation(t *testing.T) {
 						IntervalCount: MinIntervalCount - 1,
 					},
 				},
-				TransmissionRisk: MaxTransmissionRisk - 1,
 			},
 			m: fmt.Sprintf("invalid interval count, %v, must be >= %v && <= %v", MinIntervalCount-1, MinIntervalCount, MaxIntervalCount),
 		},
@@ -196,7 +194,6 @@ func TestPublishValidation(t *testing.T) {
 						IntervalCount: MaxIntervalCount + 1,
 					},
 				},
-				TransmissionRisk: MaxTransmissionRisk - 1,
 			},
 			m: fmt.Sprintf("invalid interval count, %v, must be >= %v && <= %v", MaxIntervalCount+1, MinIntervalCount, MaxIntervalCount),
 		},
@@ -210,7 +207,6 @@ func TestPublishValidation(t *testing.T) {
 						IntervalCount:  MaxIntervalCount,
 					},
 				},
-				TransmissionRisk: MaxTransmissionRisk - 1,
 			},
 			m: fmt.Sprintf("interval number %v is too old, must be >= %v", minInterval-1, minInterval),
 		},
@@ -224,7 +220,6 @@ func TestPublishValidation(t *testing.T) {
 						IntervalCount:  1,
 					},
 				},
-				TransmissionRisk: MaxTransmissionRisk - 1,
 			},
 			m: fmt.Sprintf("interval number %v is in the future, must be < %v", currentInterval+1, currentInterval),
 		},
@@ -270,52 +265,59 @@ func TestTransform(t *testing.T) {
 	source := &Publish{
 		Keys: []ExposureKey{
 			{
-				Key:            encodeKey(generateKey(t)),
-				IntervalNumber: intervalNumber,
-				IntervalCount:  MaxIntervalCount,
+				Key:              encodeKey(generateKey(t)),
+				IntervalNumber:   intervalNumber,
+				IntervalCount:    MaxIntervalCount,
+				TransmissionRisk: 1,
 			},
 			{
-				Key:            encodeKey(generateKey(t)),
-				IntervalNumber: intervalNumber + MaxIntervalCount,
-				IntervalCount:  MaxIntervalCount,
+				Key:              encodeKey(generateKey(t)),
+				IntervalNumber:   intervalNumber + MaxIntervalCount,
+				IntervalCount:    MaxIntervalCount,
+				TransmissionRisk: 2,
 			},
 			{
-				Key:            encodeKey(generateKey(t)),
-				IntervalNumber: intervalNumber + 2*MaxIntervalCount,
-				IntervalCount:  MaxIntervalCount, // Invalid, should get rounded down
+				Key:              encodeKey(generateKey(t)),
+				IntervalNumber:   intervalNumber + 2*MaxIntervalCount,
+				IntervalCount:    MaxIntervalCount, // Invalid, should get rounded down
+				TransmissionRisk: 3,
 			},
 			{
-				Key:            encodeKey(generateKey(t)),
-				IntervalNumber: intervalNumber + 3*MaxIntervalCount,
-				IntervalCount:  42,
+				Key:              encodeKey(generateKey(t)),
+				IntervalNumber:   intervalNumber + 3*MaxIntervalCount,
+				IntervalCount:    42,
+				TransmissionRisk: 4,
 			},
 		},
-		Regions:          []string{"us", "cA", "Mx"}, // will be upcased
-		AppPackageName:   "com.google",
-		TransmissionRisk: 2,
+		Regions:        []string{"us", "cA", "Mx"}, // will be upcased
+		AppPackageName: "com.google",
 		// Verification doesn't matter for transforming.
 	}
 
 	want := []*Exposure{
 		{
-			ExposureKey:    decodeKey(source.Keys[0].Key, t),
-			IntervalNumber: intervalNumber,
-			IntervalCount:  MaxIntervalCount,
+			ExposureKey:      decodeKey(source.Keys[0].Key, t),
+			IntervalNumber:   intervalNumber,
+			IntervalCount:    MaxIntervalCount,
+			TransmissionRisk: 1,
 		},
 		{
-			ExposureKey:    decodeKey(source.Keys[1].Key, t),
-			IntervalNumber: intervalNumber + MaxIntervalCount,
-			IntervalCount:  MaxIntervalCount,
+			ExposureKey:      decodeKey(source.Keys[1].Key, t),
+			IntervalNumber:   intervalNumber + MaxIntervalCount,
+			IntervalCount:    MaxIntervalCount,
+			TransmissionRisk: 2,
 		},
 		{
-			ExposureKey:    decodeKey(source.Keys[2].Key, t),
-			IntervalNumber: intervalNumber + 2*MaxIntervalCount,
-			IntervalCount:  MaxIntervalCount,
+			ExposureKey:      decodeKey(source.Keys[2].Key, t),
+			IntervalNumber:   intervalNumber + 2*MaxIntervalCount,
+			IntervalCount:    MaxIntervalCount,
+			TransmissionRisk: 3,
 		},
 		{
-			ExposureKey:    decodeKey(source.Keys[3].Key, t),
-			IntervalNumber: intervalNumber + 3*MaxIntervalCount,
-			IntervalCount:  42,
+			ExposureKey:      decodeKey(source.Keys[3].Key, t),
+			IntervalNumber:   intervalNumber + 3*MaxIntervalCount,
+			IntervalCount:    42,
+			TransmissionRisk: 4,
 		},
 	}
 	batchTime := captureStartTime.Add(time.Hour * 24 * 7)
@@ -323,7 +325,7 @@ func TestTransform(t *testing.T) {
 	for i, v := range want {
 		want[i] = &Exposure{
 			ExposureKey:      v.ExposureKey,
-			TransmissionRisk: 2,
+			TransmissionRisk: i + 1,
 			AppPackageName:   "com.google",
 			Regions:          []string{"US", "CA", "MX"},
 			IntervalNumber:   v.IntervalNumber,
@@ -373,9 +375,8 @@ func TestTransformOverlapping(t *testing.T) {
 						IntervalCount:  MaxIntervalCount,
 					},
 				},
-				Regions:          []string{"us", "cA", "Mx"}, // will be upcased
-				AppPackageName:   "com.google",
-				TransmissionRisk: 2,
+				Regions:        []string{"us", "cA", "Mx"}, // will be upcased
+				AppPackageName: "com.google",
 			},
 		},
 		{
@@ -393,9 +394,8 @@ func TestTransformOverlapping(t *testing.T) {
 						IntervalCount:  MaxIntervalCount,
 					},
 				},
-				Regions:          []string{"us", "cA", "Mx"}, // will be upcased
-				AppPackageName:   "com.google",
-				TransmissionRisk: 2,
+				Regions:        []string{"us", "cA", "Mx"}, // will be upcased
+				AppPackageName: "com.google",
 			},
 		},
 	}