Skip to content
This repository was archived by the owner on Jul 12, 2023. It is now read-only.

Commit 8294ee6

Browse files
authored
Early key release (#706)
* Allow for v1.5+ early key release. Multiple keys can be provided that all have the same start interval * Add configuration params for this * Add tests for new pieces. 100% coverage on exposure model transform. * Add documentation. * add same day release as an optional feature to the generate server Fixes #705 Part of #663
1 parent 1057fa3 commit 8294ee6

File tree

10 files changed

+249
-69
lines changed

10 files changed

+249
-69
lines changed

docs/getting-started/publishing-temporary-exposure-keys.md

+36-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,42 @@ exposed, their keys are shared to the server in order for applications to
1919
download and [determine if other users interacted with any of the now exposed
2020
keys](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf).
2121

22-
## Publishing Keys
22+
## The Publish API Call
23+
24+
TEKs are published by sending the appropriate JSON document in the body of
25+
an HTTP POST request to the `exposure` server.
26+
27+
The structure of the API is defined in [pkg/api/v1alpha1/exposure_types.go](https://github.com/google/exposure-notifications-server/blob/main/pkg/api/v1alpha1/exposure_types.go),
28+
in the `Publish` type. Please see the documentation in the source file
29+
for details of the fields themselves.
30+
31+
Here, we point out some non-obvious validation that is applied to the keys.
32+
33+
* All keys must be valid! If there are any validation errors, the entire batch
34+
is rejected.
35+
* Max keys per publish: Default is `20` and can be adjusted with the
36+
`MAX_KEYS_ON_PUBLISH` environment variable.
37+
* Max overlapping keys with same start interval: Default is `3` and can be
38+
adjusted with the `MAX_SAME_START_INTERVAL_KEYS` environment variable.
39+
In practical terms, this means that if you are obtaining TEK history on a
40+
mobile device with >= v1.5 of the device API, it will stop the validity
41+
of the current day's TEK and issue a new now. Both keys will have the same
42+
start iterval.
43+
* Max age: How old keys can be. The default is `360h` (15 days) and can be
44+
adjusted with the `MAX_INTERVAL_AGE_ON_PUBLISH`. All provided keys must have
45+
a `rollingStartNumber` that is >= to the max age.
46+
* Keys with a future start time (`rollingStartNumber` indicates time > now),
47+
are rejected.
48+
* Keys that are "sill valid" are accepted by the server, but they are embargoed
49+
until after they key could no longer be replayed usefully. A stall valid key
50+
is one where the `rollingStartNumber` is in the past, but the
51+
`rollingStartNumber` + the `rollingPeriod` indicates a future time.
52+
* When using health authority verification certificates
53+
(__strongly recommended__), the TEK data in the publish request and the
54+
`hmackey` must be able to be used to calculate the HMAC value as present in
55+
the certificate.
56+
57+
## Server Access Configuration
2358

2459
In order for your application to publish keys to the server, the server
2560
requires the registration of the Application Name (for Android) or the Bundle ID

internal/generate/config.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ type Config struct {
3636
SecretManager secrets.Config
3737
ObservabilityExporter observability.Config
3838

39-
Port string `env:"PORT, default=8080"`
40-
NumExposures int `env:"NUM_EXPOSURES_GENERATED, default=10"`
41-
KeysPerExposure int `env:"KEYS_PER_EXPOSURE, default=14"`
42-
MaxKeysOnPublish int `env:"MAX_KEYS_ON_PUBLISH, default=15"`
43-
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
44-
TruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`
45-
DefaultRegion string `env:"DEFAULT_REGOIN, default=US"`
39+
Port string `env:"PORT, default=8080"`
40+
NumExposures int `env:"NUM_EXPOSURES_GENERATED, default=10"`
41+
KeysPerExposure int `env:"KEYS_PER_EXPOSURE, default=14"`
42+
MaxKeysOnPublish int `env:"MAX_KEYS_ON_PUBLISH, default=15"`
43+
MaxSameStartIntervalKeys int `env:"MAX_SAME_START_INTERVAL_KEYS, default=2"`
44+
SimulateSameDayRelease bool `env:"SIMULATE_SAME_DAY_RELEASE, default=false"`
45+
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
46+
TruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`
47+
DefaultRegion string `env:"DEFAULT_REGOIN, default=US"`
4648
}
4749

4850
func (c *Config) DatabaseConfig() *database.Config {

internal/generate/handler.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"fmt"
2020
"net/http"
21+
"sort"
2122
"strings"
2223
"time"
2324

@@ -38,7 +39,7 @@ func NewHandler(ctx context.Context, config *Config, env *serverenv.ServerEnv) (
3839
return nil, fmt.Errorf("missing database in server environment")
3940
}
4041

41-
transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxIntervalAge, config.TruncateWindow, false)
42+
transformer, err := model.NewTransformer(config.MaxKeysOnPublish, config.MaxSameStartIntervalKeys, config.MaxIntervalAge, config.TruncateWindow, false)
4243
if err != nil {
4344
return nil, fmt.Errorf("model.NewTransformer: %w", err)
4445
}
@@ -86,6 +87,19 @@ func (h *generateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
8687
Regions: regions,
8788
AppPackageName: "generated.data",
8889
}
90+
if h.config.SimulateSameDayRelease {
91+
sort.Slice(publish.Keys, func(i int, j int) bool {
92+
return publish.Keys[i].IntervalNumber < publish.Keys[j].IntervalNumber
93+
})
94+
lastKey := &publish.Keys[len(publish.Keys)-1]
95+
newLastDayKey, err := util.RandomExposureKey(lastKey.IntervalNumber, 144, lastKey.TransmissionRisk)
96+
if err != nil {
97+
logger.Errorf("unable to simulate same day key release: %v", err)
98+
} else {
99+
lastKey.IntervalCount = lastKey.IntervalCount / 2
100+
publish.Keys = append(publish.Keys, newLastDayKey)
101+
}
102+
}
89103

90104
exposures, err := h.transformer.TransformPublish(ctx, &publish, batchTime)
91105
if err != nil {

internal/integration/integration.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,11 @@ func testServer(tb testing.TB) (*serverenv.ServerEnv, *http.Client) {
151151

152152
// Publish
153153
publishConfig := &publish.Config{
154-
MaxKeysOnPublish: 15,
155-
MaxIntervalAge: 360 * time.Hour,
156-
TruncateWindow: 1 * time.Second,
157-
DebugReleaseSameDayKeys: true,
154+
MaxKeysOnPublish: 15,
155+
MaxSameStartIntervalKeys: 2,
156+
MaxIntervalAge: 360 * time.Hour,
157+
TruncateWindow: 1 * time.Second,
158+
DebugReleaseSameDayKeys: true,
158159
}
159160

160161
publishHandler, err := publish.NewHandler(ctx, publishConfig, env)

internal/publish/config.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ type Config struct {
4141
Verification verification.Config
4242
ObservabilityExporter observability.Config
4343

44-
Port string `env:"PORT, default=8080"`
45-
MaxKeysOnPublish int `env:"MAX_KEYS_ON_PUBLISH, default=15"`
46-
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
47-
TruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`
44+
Port string `env:"PORT, default=8080"`
45+
MaxKeysOnPublish int `env:"MAX_KEYS_ON_PUBLISH, default=20"`
46+
// Provides compatibility w/ 1.5 release.
47+
MaxSameStartIntervalKeys int `env:"MAX_SAME_START_INTERVAL_KEYS, default=3"`
48+
MaxIntervalAge time.Duration `env:"MAX_INTERVAL_AGE_ON_PUBLISH, default=360h"`
49+
TruncateWindow time.Duration `env:"TRUNCATE_WINDOW, default=1h"`
4850

49-
// Flags for local development and testing.
51+
// Flags for local development and testing. This will cause still valid keys
52+
// to not be embargoed.
53+
// Normallly "still valid" keys can be accepted, but are embargoed.
5054
DebugReleaseSameDayKeys bool `env:"DEBUG_RELEASE_SAME_DAY_KEYS"`
5155
}
5256

internal/publish/model/exposure_model.go

+46-14
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ func TimeForIntervalNumber(interval int32) time.Time {
103103

104104
// Transformer represents a configured Publish -> Exposure[] transformer.
105105
type Transformer struct {
106-
maxExposureKeys int
106+
maxExposureKeys int // Overall maximum number of keys.
107+
maxSameDayKeys int // Number of keys that are allowed to have the same start interval.
107108
maxIntervalStartAge time.Duration // How many intervals old does this server accept?
108109
truncateWindow time.Duration
109110
debugReleaseSameDay bool // If true, still valid keys are not embargoed.
@@ -112,18 +113,23 @@ type Transformer struct {
112113
// NewTransformer creates a transformer for turning publish API requests into
113114
// records for insertion into the database. On the call to TransformPublish
114115
// all data is validated according to the transformer that is used.
115-
func NewTransformer(maxExposureKeys int, maxIntervalStartAge time.Duration, truncateWindow time.Duration, releaseSameDayKeys bool) (*Transformer, error) {
116+
func NewTransformer(maxExposureKeys int, maxSameDayKeys int, maxIntervalStartAge time.Duration, truncateWindow time.Duration, releaseSameDayKeys bool) (*Transformer, error) {
116117
if maxExposureKeys <= 0 {
117118
return nil, fmt.Errorf("maxExposureKeys must be > 0, got %v", maxExposureKeys)
118119
}
120+
if maxSameDayKeys < 1 {
121+
return nil, fmt.Errorf("maxSameDayKeys must be >= 1, got %v", maxSameDayKeys)
122+
}
119123
return &Transformer{
120124
maxExposureKeys: maxExposureKeys,
125+
maxSameDayKeys: maxSameDayKeys,
121126
maxIntervalStartAge: maxIntervalStartAge,
122127
truncateWindow: truncateWindow,
123128
debugReleaseSameDay: releaseSameDayKeys,
124129
}, nil
125130
}
126131

132+
// KeyTransform represents the settings to apply when transforming an individual key on a publish request.
127133
type KeyTransform struct {
128134
MinStartInterval int32
129135
MaxStartInterval int32
@@ -165,7 +171,7 @@ func TransformExposureKey(exposureKey verifyapi.ExposureKey, appPackageName stri
165171
createdAt := settings.CreatedAt
166172
// If the key is valid beyond the current interval number. Adjust the createdAt time for the key.
167173
if exposureKey.IntervalNumber+exposureKey.IntervalCount > settings.MaxStartInterval {
168-
// key is still valid. The created At for this key needs to be adjusted unless debuggin is enabled.
174+
// key is still valid. The created At for this key needs to be adjusted unless debugging is enabled.
169175
if !settings.ReleaseStillValidKeys {
170176
createdAt = TimeForIntervalNumber(exposureKey.IntervalNumber + exposureKey.IntervalCount).Truncate(settings.BatchWindow)
171177
}
@@ -238,25 +244,51 @@ func (t *Transformer) TransformPublish(ctx context.Context, inData *verifyapi.Pu
238244
entities = append(entities, exposure)
239245
}
240246

241-
// Ensure that the uploaded keys are for a consecutive time period. No
242-
// overlaps and no gaps.
243-
// 1) Sort by interval number.
247+
// Validate the uploaded data meets configuration parameters.
248+
// In v1.5+, it is possible to have multiple keys that overlap. They
249+
// take the form of the same start interval with variable rolling period numbers.
250+
// Sort by interval number to make necessary checks easier.
244251
sort.Slice(entities, func(i int, j int) bool {
252+
if entities[i].IntervalNumber == entities[j].IntervalNumber {
253+
return entities[i].IntervalCount == entities[j].IntervalCount
254+
}
245255
return entities[i].IntervalNumber < entities[j].IntervalNumber
246256
})
247-
// 2) Walk the slice and verify no gaps/overlaps.
248-
// We know the slice isn't empty, seed w/ the first interval.
249-
nextInterval := entities[0].IntervalNumber
257+
// Check that any overlapping keys meet configuration.
258+
// Overlapping keys must have the same start interval. And there is a max number
259+
// of "same day" keys that are allowed.
260+
// We do not enforce that keys have UTC midnight aligned start intervals.
261+
262+
// Running count of start intervals.
263+
startIntervals := make(map[int32]int)
264+
lastInterval := entities[0].IntervalNumber
265+
nextInterval := entities[0].IntervalNumber + entities[0].IntervalCount
266+
250267
for _, ex := range entities {
268+
// Relies on the default value of 0 for the map value type.
269+
startIntervals[ex.IntervalNumber] = startIntervals[ex.IntervalNumber] + 1
270+
271+
if ex.IntervalNumber == lastInterval {
272+
// OK, overlaps by start interval. But move out the nextInterval
273+
nextInterval = ex.IntervalNumber + ex.IntervalCount
274+
continue
275+
}
276+
251277
if ex.IntervalNumber < nextInterval {
252-
if t.debugReleaseSameDay {
253-
logging.FromContext(ctx).Errorf("exposure keys have overlapping intervals")
254-
break
255-
}
256-
return nil, fmt.Errorf("exposure keys have overlapping intervals")
278+
msg := fmt.Sprintf("exposure keys have non aligned overlapping intervals. %v overlaps with previous key that is good from %v to %v.", ex.IntervalNumber, lastInterval, nextInterval)
279+
logging.FromContext(ctx).Errorf(msg)
280+
return nil, fmt.Errorf(msg)
257281
}
282+
// OK, current key starts at or after the end of the previous one. Advance both variables.
283+
lastInterval = ex.IntervalNumber
258284
nextInterval = ex.IntervalNumber + ex.IntervalCount
259285
}
260286

287+
for k, v := range startIntervals {
288+
if v > t.maxSameDayKeys {
289+
return nil, fmt.Errorf("too many overlapping keys for start interval: %v want: <= %v, got: %v", k, t.maxSameDayKeys, v)
290+
}
291+
}
292+
261293
return entities, nil
262294
}

0 commit comments

Comments
 (0)