Repository: google/exposure-notifications-server
Pull request #352 (2020-05-19 00:50:53)

Rename/prefix SafetyNet columns
This updates the `AuthorizedApp` table to prefix the SafetyNet-related columns with Android and updates references.

Per discussion, it also changes the behavior of the `allowed_regions` field and drops the `all_regions` boolean in favor of the follow behavior:

- The list cannot be null (database constraint)
- If the list is empty, all regions are allowed
- If the list contains any values, only exposures from that region are allowed

Fixes GH-269

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/352#" title="Author self-approved">sethvargo</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)~~ [sethvargo]

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":[]} -->
sethvargo: /assign @mikehelmick 
google-prow-robot: @sethvargo: The following test **failed**, say `/retest` to rerun all failed tests:

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

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

<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 -->
sethvargo: /assign @mikehelmick 
mikehelmick: /lgtm
/approve
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/352#issuecomment-630939089" title="Approved">mikehelmick</a>*, *<a href="https://github.com/google/exposure-notifications-server/pull/352#" title="Author self-approved">sethvargo</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,sethvargo]

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":[]} -->

Patch:
diff --git a/internal/android/safetynet.go b/internal/android/safetynet.go
index a0cd834..ee963c4 100644
--- a/internal/android/safetynet.go
+++ b/internal/android/safetynet.go
@@ -140,23 +140,23 @@ func ValidateAttestation(ctx context.Context, attestation string, opts *VerifyOp
 // VerifyOptsFor returns the Android SafetyNet verification options to be used
 // based on the AuthorizedApp configuration, request time, and nonce.
 func VerifyOptsFor(c *database.AuthorizedApp, from time.Time, nonce string) *VerifyOpts {
-	digests := make([]string, len(c.ApkDigestSHA256))
-	copy(digests, c.ApkDigestSHA256)
+	digests := make([]string, len(c.SafetyNetApkDigestSHA256))
+	copy(digests, c.SafetyNetApkDigestSHA256)
 	rtn := &VerifyOpts{
 		AppPkgName:      c.AppPackageName,
-		CTSProfileMatch: c.CTSProfileMatch,
-		BasicIntegrity:  c.BasicIntegrity,
+		BasicIntegrity:  c.SafetyNetBasicIntegrity,
+		CTSProfileMatch: c.SafetyNetCTSProfileMatch,
 		APKDigest:       digests,
 		Nonce:           nonce,
 	}
 
 	// Calculate the valid time window based on now + config options.
-	if c.AllowedPastTime > 0 {
-		minTime := from.Add(-c.AllowedPastTime)
+	if c.SafetyNetPastTime > 0 {
+		minTime := from.Add(-c.SafetyNetPastTime)
 		rtn.MinValidTime = minTime
 	}
-	if c.AllowedFutureTime > 0 {
-		maxTime := from.Add(c.AllowedFutureTime)
+	if c.SafetyNetFutureTime > 0 {
+		maxTime := from.Add(c.SafetyNetFutureTime)
 		rtn.MaxValidTime = maxTime
 	}
 
diff --git a/internal/database/authorized_app.go b/internal/database/authorized_app.go
index ff1b54f..efc1073 100644
--- a/internal/database/authorized_app.go
+++ b/internal/database/authorized_app.go
@@ -36,9 +36,9 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 
 	query := `
 		SELECT
-			app_package_name, platform, apk_digest, cts_profile_match, basic_integrity,
-			allowed_past_seconds, allowed_future_seconds, allowed_regions, all_regions,
-			ios_devicecheck_team_id_secret, ios_devicecheck_key_id_secret, ios_devicecheck_private_key_secret
+			app_package_name, platform, allowed_regions,
+			safetynet_apk_digest, safetynet_cts_profile_match, safetynet_basic_integrity, safetynet_past_seconds, safetynet_future_seconds,
+			devicecheck_team_id_secret, devicecheck_key_id_secret, devicecheck_private_key_secret
 		FROM
 			AuthorizedApp
 		WHERE app_package_name = $1`
@@ -47,11 +47,11 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 
 	config := NewAuthorizedApp()
 	var allowedRegions []string
-	var allowedPastSeconds, allowedFutureSeconds *int
+	var safetyNetPastSeconds, safetyNetFutureSeconds *int
 	var deviceCheckTeamIDSecret, deviceCheckKeyIDSecret, deviceCheckPrivateKeySecret sql.NullString
 	if err := row.Scan(
-		&config.AppPackageName, &config.Platform, &config.ApkDigestSHA256, &config.CTSProfileMatch, &config.BasicIntegrity,
-		&allowedPastSeconds, &allowedFutureSeconds, &allowedRegions, &config.AllowAllRegions,
+		&config.AppPackageName, &config.Platform, &allowedRegions,
+		&config.SafetyNetApkDigestSHA256, &config.SafetyNetCTSProfileMatch, &config.SafetyNetBasicIntegrity, &safetyNetPastSeconds, &safetyNetFutureSeconds,
 		&deviceCheckTeamIDSecret, &deviceCheckKeyIDSecret, &deviceCheckPrivateKeySecret,
 	); err != nil {
 		if err == pgx.ErrNoRows {
@@ -61,13 +61,13 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 	}
 
 	// Convert time in seconds from DB into time.Duration
-	if allowedPastSeconds != nil {
-		d := time.Duration(*allowedPastSeconds) * time.Second
-		config.AllowedPastTime = d
+	if safetyNetPastSeconds != nil {
+		d := time.Duration(*safetyNetPastSeconds) * time.Second
+		config.SafetyNetPastTime = d
 	}
-	if allowedFutureSeconds != nil {
-		d := time.Duration(*allowedFutureSeconds) * time.Second
-		config.AllowedFutureTime = d
+	if safetyNetFutureSeconds != nil {
+		d := time.Duration(*safetyNetFutureSeconds) * time.Second
+		config.SafetyNetFutureTime = d
 	}
 
 	// build the regions map
@@ -79,7 +79,7 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 	if v := deviceCheckTeamIDSecret; v.Valid && v.String != "" {
 		plaintext, err := sm.GetSecretValue(ctx, v.String)
 		if err != nil {
-			return nil, fmt.Errorf("ios_devicecheck_team_id_secret at %s (%s): %w",
+			return nil, fmt.Errorf("devicecheck_team_id_secret at %s (%s): %w",
 				config.AppPackageName, config.Platform, err)
 		}
 		config.DeviceCheckTeamID = plaintext
@@ -88,7 +88,7 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 	if v := deviceCheckKeyIDSecret; v.Valid && v.String != "" {
 		plaintext, err := sm.GetSecretValue(ctx, v.String)
 		if err != nil {
-			return nil, fmt.Errorf("ios_devicecheck_key_id_secret at %s (%s): %w",
+			return nil, fmt.Errorf("devicecheck_key_id_secret at %s (%s): %w",
 				config.AppPackageName, config.Platform, err)
 		}
 		config.DeviceCheckKeyID = plaintext
@@ -97,7 +97,7 @@ func (db *DB) GetAuthorizedApp(ctx context.Context, sm secrets.SecretManager, na
 	if v := deviceCheckPrivateKeySecret; v.Valid && v.String != "" {
 		plaintext, err := sm.GetSecretValue(ctx, v.String)
 		if err != nil {
-			return nil, fmt.Errorf("ios_devicecheck_private_key_secret at %s (%s): %w",
+			return nil, fmt.Errorf("devicecheck_private_key_secret at %s (%s): %w",
 				config.AppPackageName, config.Platform, err)
 		}
 
diff --git a/internal/database/authorized_app_model.go b/internal/database/authorized_app_model.go
index 5d5fed2..5bbb654 100644
--- a/internal/database/authorized_app_model.go
+++ b/internal/database/authorized_app_model.go
@@ -28,18 +28,22 @@ const (
 // application and their access to and requirements for using the API. DB times
 // of 0 are interpreted to be "unbounded" in that direction.
 type AuthorizedApp struct {
-	AppPackageName  string
-	Platform        string
-	AllowedRegions  map[string]struct{}
-	AllowAllRegions bool
+	// AppPackageName is the name of the package like com.company.app.
+	AppPackageName string
+
+	// Platform is the app platform like "android" or "ios".
+	Platform string
+
+	// AllowedRegions is the list of allowed regions for this app. If the list is
+	// empty, all regions are permitted.
+	AllowedRegions map[string]struct{}
 
 	// SafetyNet configuration.
-	// TODO(sethvargo): Rename these to clarify they are for SafetyNet.
-	AllowedPastTime   time.Duration
-	AllowedFutureTime time.Duration
-	ApkDigestSHA256   []string
-	BasicIntegrity    bool
-	CTSProfileMatch   bool
+	SafetyNetApkDigestSHA256 []string
+	SafetyNetBasicIntegrity  bool
+	SafetyNetCTSProfileMatch bool
+	SafetyNetPastTime        time.Duration
+	SafetyNetFutureTime      time.Duration
 
 	// DeviceCheck configuration.
 	DeviceCheckKeyID      string
@@ -63,9 +67,13 @@ func (c *AuthorizedApp) IsAndroid() bool {
 	return c.Platform == androidDevice
 }
 
-// IsAllowedRegion returns true if the region is in the list of allowed regions,
-// false otherwise.
+// IsAllowedRegion returns true if the regions list is empty or if the given
+// region is in the list of allowed regions.
 func (c *AuthorizedApp) IsAllowedRegion(s string) bool {
+	if len(c.AllowedRegions) == 0 {
+		return true
+	}
+
 	_, ok := c.AllowedRegions[s]
 	return ok
 }
diff --git a/internal/verification/verify.go b/internal/verification/verify.go
index 3258830..a8c4434 100644
--- a/internal/verification/verify.go
+++ b/internal/verification/verify.go
@@ -33,16 +33,12 @@ var (
 // the configuration for the application.
 func VerifyRegions(cfg *database.AuthorizedApp, data *database.Publish) error {
 	if cfg == nil {
-		return fmt.Errorf("no allowed regions configured")
-	}
-
-	if cfg.AllowAllRegions {
-		return nil
+		return fmt.Errorf("app configuration is empty")
 	}
 
 	for _, r := range data.Regions {
 		if !cfg.IsAllowedRegion(r) {
-			return fmt.Errorf("application '%v' tried to write unauthorized region: '%v'", cfg.AppPackageName, r)
+			return fmt.Errorf("app '%v' tried to write unauthorized region: '%v'", cfg.AppPackageName, r)
 		}
 	}
 
diff --git a/migrations/000022_authorized_app_prefixes.down.sql b/migrations/000022_authorized_app_prefixes.down.sql
new file mode 100644
index 0000000..3af5c7d
--- /dev/null
+++ b/migrations/000022_authorized_app_prefixes.down.sql
@@ -0,0 +1,33 @@
+-- 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
+--
+--      http://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.
+
+BEGIN;
+
+ALTER TABLE AuthorizedApp RENAME COLUMN safetynet_apk_digest TO apk_digest;
+ALTER TABLE AuthorizedApp RENAME COLUMN safetynet_cts_profile_match TO cts_profile_match;
+ALTER TABLE AuthorizedApp RENAME COLUMN safetynet_basic_integrity TO basic_integrity;
+ALTER TABLE AuthorizedApp RENAME COLUMN safetynet_past_seconds TO allowed_past_seconds;
+ALTER TABLE AuthorizedApp RENAME COLUMN safetynet_future_seconds TO allowed_future_seconds;
+ALTER TABLE AuthorizedApp RENAME COLUMN devicecheck_team_id_secret TO ios_devicecheck_team_id_secret;
+ALTER TABLE AuthorizedApp RENAME COLUMN devicecheck_key_id_secret TO ios_devicecheck_key_id_secret;
+ALTER TABLE AuthorizedApp RENAME COLUMN devicecheck_private_key_secret TO ios_devicecheck_private_key_secret;
+
+-- Update the semantic meaning of all_regions
+ALTER TABLE AuthorizedApp ADD COLUMN all_regions bool DEFAULT false;
+UPDATE AuthorizedApp SET all_regions = true
+WHERE allowed_regions = '{}';
+
+CREATE INDEX authorized_app_app_package_name_idx ON AuthorizedApp(app_package_name);
+
+END;
diff --git a/migrations/000022_authorized_app_prefixes.up.sql b/migrations/000022_authorized_app_prefixes.up.sql
new file mode 100644
index 0000000..fff22e3
--- /dev/null
+++ b/migrations/000022_authorized_app_prefixes.up.sql
@@ -0,0 +1,32 @@
+-- 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
+--
+--      http://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.
+
+BEGIN;
+
+-- Update the semantic meaning of all_regions
+UPDATE AuthorizedApp SET allowed_regions = ARRAY[]::VARCHAR[]
+WHERE all_regions = true;
+
+ALTER TABLE AuthorizedApp RENAME COLUMN apk_digest TO safetynet_apk_digest;
+ALTER TABLE AuthorizedApp RENAME COLUMN cts_profile_match TO safetynet_cts_profile_match;
+ALTER TABLE AuthorizedApp RENAME COLUMN basic_integrity TO safetynet_basic_integrity;
+ALTER TABLE AuthorizedApp RENAME COLUMN allowed_past_seconds TO safetynet_past_seconds;
+ALTER TABLE AuthorizedApp RENAME COLUMN allowed_future_seconds TO safetynet_future_seconds;
+ALTER TABLE AuthorizedApp RENAME COLUMN ios_devicecheck_team_id_secret TO devicecheck_team_id_secret;
+ALTER TABLE AuthorizedApp RENAME COLUMN ios_devicecheck_key_id_secret TO devicecheck_key_id_secret;
+ALTER TABLE AuthorizedApp RENAME COLUMN ios_devicecheck_private_key_secret TO devicecheck_private_key_secret;
+ALTER TABLE AuthorizedApp DROP COLUMN all_regions;
+DROP INDEX authorized_app_app_package_name_idx;
+
+END;


Test patch:
diff --git a/internal/android/safetynet_test.go b/internal/android/safetynet_test.go
index cec9651..f68419a 100644
--- a/internal/android/safetynet_test.go
+++ b/internal/android/safetynet_test.go
@@ -197,11 +197,11 @@ func TestVerifyOptsFor(t *testing.T) {
 	}{
 		{
 			cfg: &database.AuthorizedApp{
-				AppPackageName:    "foo",
-				CTSProfileMatch:   true,
-				BasicIntegrity:    true,
-				AllowedPastTime:   time.Duration(15 * time.Minute),
-				AllowedFutureTime: time.Duration(1 * time.Second),
+				AppPackageName:           "foo",
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: true,
+				SafetyNetPastTime:        time.Duration(15 * time.Minute),
+				SafetyNetFutureTime:      time.Duration(1 * time.Second),
 			},
 			opts: &VerifyOpts{
 				AppPkgName:      "foo",
@@ -214,29 +214,29 @@ func TestVerifyOptsFor(t *testing.T) {
 		},
 		{
 			cfg: &database.AuthorizedApp{
-				AppPackageName:    "foo",
-				CTSProfileMatch:   false,
-				BasicIntegrity:    true,
-				AllowedPastTime:   0,
-				AllowedFutureTime: 0,
+				AppPackageName:           "foo",
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: false,
+				SafetyNetPastTime:        0,
+				SafetyNetFutureTime:      0,
 			},
 			opts: &VerifyOpts{
 				AppPkgName:      "foo",
 				APKDigest:       []string{},
-				CTSProfileMatch: false,
 				BasicIntegrity:  true,
+				CTSProfileMatch: false,
 				MinValidTime:    time.Time{},
 				MaxValidTime:    time.Time{},
 			},
 		},
 		{
 			cfg: &database.AuthorizedApp{
-				AppPackageName:    "foo",
-				ApkDigestSHA256:   []string{"bar"},
-				CTSProfileMatch:   false,
-				BasicIntegrity:    true,
-				AllowedPastTime:   0,
-				AllowedFutureTime: 0,
+				AppPackageName:           "foo",
+				SafetyNetApkDigestSHA256: []string{"bar"},
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: false,
+				SafetyNetPastTime:        0,
+				SafetyNetFutureTime:      0,
 			},
 			opts: &VerifyOpts{
 				AppPkgName:      "foo",
diff --git a/internal/database/authorized_app_test.go b/internal/database/authorized_app_test.go
index d6b9c23..7429628 100644
--- a/internal/database/authorized_app_test.go
+++ b/internal/database/authorized_app_test.go
@@ -83,69 +83,111 @@ func TestGetAuthorizedApp(t *testing.T) {
 				INSERT INTO AuthorizedApp (app_package_name, platform, allowed_regions)
 				VALUES ($1, $2, $3)
 			`,
-			args: []interface{}{"myapp", "ios", []string{"US"}},
+
+			args: []interface{}{"myapp", "android", []string{"US"}},
+			exp: &AuthorizedApp{
+				AppPackageName:           "myapp",
+				Platform:                 "android",
+				AllowedRegions:           map[string]struct{}{"US": {}},
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: true,
+			},
+		},
+		{
+			name: "all_regions",
+			sql: `
+				INSERT INTO AuthorizedApp (app_package_name, platform, allowed_regions)
+				VALUES ($1, $2, $3)
+			`,
+			args: []interface{}{"myapp", "android", []string{}},
 			exp: &AuthorizedApp{
-				AppPackageName:  "myapp",
-				Platform:        "ios",
-				AllowedRegions:  map[string]struct{}{"US": {}},
-				CTSProfileMatch: true,
-				BasicIntegrity:  true,
+				AppPackageName:           "myapp",
+				Platform:                 "android",
+				AllowedRegions:           map[string]struct{}{},
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: true,
 			},
 		},
 		{
-			name: "allowed_past_time",
+			name: "safetynet_fileds",
+			sql: `
+				INSERT INTO AuthorizedApp (
+					app_package_name, platform, allowed_regions,
+					safetynet_apk_digest, safetynet_cts_profile_match, safetynet_basic_integrity
+				)
+				VALUES (
+					$1, $2, $3,
+					$4, $5, $6
+				)
+			`,
+			args: []interface{}{
+				"myapp", "android", []string{},
+				[]string{"092fcfb", "252f10c"}, false, false,
+			},
+			exp: &AuthorizedApp{
+				AppPackageName:           "myapp",
+				Platform:                 "android",
+				AllowedRegions:           map[string]struct{}{},
+				SafetyNetApkDigestSHA256: []string{"092fcfb", "252f10c"},
+				SafetyNetBasicIntegrity:  false,
+				SafetyNetCTSProfileMatch: false,
+			},
+		},
+
+		{
+			name: "safetynet_past_seconds",
 			sql: `
 				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
-					allowed_past_seconds
+					safetynet_past_seconds
 				) VALUES ($1, $2, $3, $4)
 			`,
-			args: []interface{}{"myapp", "ios", []string{"US"}, 1800},
+			args: []interface{}{"myapp", "android", []string{"US"}, 1800},
 			exp: &AuthorizedApp{
-				AppPackageName:  "myapp",
-				Platform:        "ios",
-				AllowedRegions:  map[string]struct{}{"US": {}},
-				CTSProfileMatch: true,
-				BasicIntegrity:  true,
-				AllowedPastTime: 30 * time.Minute,
+				AppPackageName:           "myapp",
+				Platform:                 "android",
+				AllowedRegions:           map[string]struct{}{"US": {}},
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: true,
+				SafetyNetPastTime:        30 * time.Minute,
 			},
 		},
 		{
-			name: "allowed_future_time",
+			name: "safetynet_future_seconds",
 			sql: `
 				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
-					allowed_future_seconds
+					safetynet_future_seconds
 				) VALUES ($1, $2, $3, $4)
 			`,
-			args: []interface{}{"myapp", "ios", []string{"US"}, 1800},
+			args: []interface{}{"myapp", "android", []string{"US"}, 1800},
 			exp: &AuthorizedApp{
-				AppPackageName:    "myapp",
-				Platform:          "ios",
-				AllowedRegions:    map[string]struct{}{"US": {}},
-				CTSProfileMatch:   true,
-				BasicIntegrity:    true,
-				AllowedFutureTime: 30 * time.Minute,
+				AppPackageName:           "myapp",
+				Platform:                 "android",
+				AllowedRegions:           map[string]struct{}{"US": {}},
+				SafetyNetBasicIntegrity:  true,
+				SafetyNetCTSProfileMatch: true,
+				SafetyNetFutureTime:      30 * time.Minute,
 			},
 		},
 		{
-			name: "ios_devicecheck",
+			name: "devicecheck",
 			sql: `
 				INSERT INTO AuthorizedApp (
 					app_package_name, platform, allowed_regions,
-					ios_devicecheck_team_id_secret, ios_devicecheck_key_id_secret, ios_devicecheck_private_key_secret
+					devicecheck_team_id_secret, devicecheck_key_id_secret, devicecheck_private_key_secret
 				) VALUES ($1, $2, $3, $4, $5, $6)
 			`,
 			args: []interface{}{"myapp", "ios", []string{"US"}, "team_id", "key_id", "private_key"},
 			exp: &AuthorizedApp{
-				AppPackageName:        "myapp",
-				Platform:              "ios",
-				AllowedRegions:        map[string]struct{}{"US": {}},
-				CTSProfileMatch:       true,
-				BasicIntegrity:        true,
-				DeviceCheckTeamID:     "ABCD1234",
-				DeviceCheckKeyID:      "DEFG5678",
-				DeviceCheckPrivateKey: p8PrivateKey,
+				AppPackageName:           "myapp",
+				Platform:                 "ios",
+				AllowedRegions:           map[string]struct{}{"US": {}},
+				SafetyNetCTSProfileMatch: true,
+				SafetyNetBasicIntegrity:  true,
+				DeviceCheckTeamID:        "ABCD1234",
+				DeviceCheckKeyID:         "DEFG5678",
+				DeviceCheckPrivateKey:    p8PrivateKey,
 			},
 		},
 		{
diff --git a/internal/verification/verify_test.go b/internal/verification/verify_test.go
index c608e11..51ea560 100644
--- a/internal/verification/verify_test.go
+++ b/internal/verification/verify_test.go
@@ -30,68 +30,95 @@ const (
 )
 
 func TestVerifyRegions(t *testing.T) {
-	allRegions := &database.AuthorizedApp{
-		AppPackageName:  appPkgName,
-		AllowAllRegions: true,
-	}
-	usCaRegions := &database.AuthorizedApp{
-		AppPackageName: appPkgName,
-		AllowedRegions: make(map[string]struct{}),
-	}
-	usCaRegions.AllowedRegions["US"] = struct{}{}
-	usCaRegions.AllowedRegions["CA"] = struct{}{}
-
 	cases := []struct {
-		Data *database.Publish
-		Msg  string
-		Cfg  *database.AuthorizedApp
+		name string
+		data *database.Publish
+		cfg  *database.AuthorizedApp
+		err  bool
 	}{
 		{
-			&database.Publish{Regions: []string{"US"}},
-			"no allowed regions configured",
-			nil,
+			name: "nil_config",
+			data: &database.Publish{Regions: []string{"US"}},
+			cfg:  nil,
+			err:  true,
 		},
 		{
-			&database.Publish{Regions: []string{"US"}},
-			"",
-			allRegions,
+			name: "nil_regions_allows_all",
+			data: &database.Publish{Regions: []string{"US"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+			},
 		},
 		{
-			&database.Publish{Regions: []string{"US"}},
-			"",
-			usCaRegions,
+			name: "empty_regions_allows_all",
+			data: &database.Publish{Regions: []string{"US"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+				AllowedRegions: map[string]struct{}{},
+			},
 		},
 		{
-			&database.Publish{Regions: []string{"US", "CA"}},
-			"",
-			usCaRegions,
+			name: "region_matches_one",
+			data: &database.Publish{Regions: []string{"US"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+				AllowedRegions: map[string]struct{}{
+					"US": {},
+					"CA": {},
+				},
+			},
+		},
+		{
+			name: "region_matches_all",
+			data: &database.Publish{Regions: []string{"US", "CA"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+				AllowedRegions: map[string]struct{}{
+					"US": {},
+					"CA": {},
+				},
+			},
 		},
 		{
-			&database.Publish{Regions: []string{"MX"}},
-			fmt.Sprintf("application '%v' tried to write unauthorized region: '%v'", appPkgName, "MX"),
-			usCaRegions,
+			name: "region_matches_some",
+			data: &database.Publish{Regions: []string{"US", "MX"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+				AllowedRegions: map[string]struct{}{
+					"US": {},
+					"CA": {},
+				},
+			},
+			err: true,
+		},
+		{
+			name: "region_matches_none",
+			data: &database.Publish{Regions: []string{"MX"}},
+			cfg: &database.AuthorizedApp{
+				AppPackageName: appPkgName,
+				AllowedRegions: map[string]struct{}{
+					"US": {},
+					"CA": {},
+				},
+			},
+			err: true,
 		},
 	}
 
-	for i, c := range cases {
-		err := VerifyRegions(c.Cfg, c.Data)
-		if c.Msg == "" && err == nil {
-			continue
-		}
-		if c.Msg == "" && err != nil {
-			t.Errorf("%v got %v, wanted no error", i, err)
-			continue
-		}
-		if err.Error() != c.Msg {
-			t.Errorf("%v wrong error, got %v, want %v", i, err, c.Msg)
-		}
+	for _, tc := range cases {
+		tc := tc
+
+		t.Run(tc.name, func(t *testing.T) {
+			if err := VerifyRegions(tc.cfg, tc.data); (err != nil) != tc.err {
+				t.Fatal(err)
+			}
+		})
 	}
 }
 
 func TestVerifySafetyNet(t *testing.T) {
 	allRegions := &database.AuthorizedApp{
-		AppPackageName:  appPkgName,
-		AllowAllRegions: true,
+		AppPackageName: appPkgName,
 	}
 
 	cases := []struct {
diff --git a/testdata/config.sql b/testdata/config.sql
index 3ad0868..60428b4 100644
--- a/testdata/config.sql
+++ b/testdata/config.sql
@@ -1,6 +1,29 @@
+-- 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
+--
+--      http://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.
 
 -- Use this as a template to inject configurations for your publish API endpoint.
 
-INSERT INTO apiconfig (app_package_name, platform, cts_profile_match, basic_integrity, allowed_past_seconds, allowed_future_seconds, allowed_regions, all_regions, ios_devicecheck_team_id_secret, ios_devicecheck_key_id_secret, ios_devicecheck_private_key_secret) VALUES
-  ('com.example.ios.app', 'ios', false, false, 60, 60, ARRAY ['GB','US'], false, 'projects/38554818207/secrets/ios-devicecheck-team-id/versions/1', 'projects/38554818207/secrets/ios-devicecheck-key-id/versions/1', 'projects/38554818207/secrets/ios-devicecheck-private-key/versions/1'),
-  ('com.example.android.app', 'android', false, false, 60, 60, ARRAY ['GB','US'], false, NULL, NULL, NULL);
+INSERT INTO AuthorizedApp (
+  app_package_name, platform, allowed_regions,
+  safetynet_cts_profile_match, safetynet_basic_integrity, safetynet_past_seconds, safetynet_future_seconds,
+  devicecheck_team_id_secret, devicecheck_key_id_secret, devicecheck_private_key_secret
+) VALUES (
+  'com.example.ios.app', 'ios', ARRAY[]::VARCHAR[],
+  false, false, 60, 60,
+  'projects/38554818207/secrets/ios-devicecheck-team-id/versions/1', 'projects/38554818207/secrets/ios-devicecheck-key-id/versions/1', 'projects/38554818207/secrets/ios-devicecheck-private-key/versions/1'
+), (
+  'com.example.android.app', 'android', ARRAY[]::VARCHAR[],
+  false, false, 60, 60,
+  NULL, NULL, NULL
+);