Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanups from feature/insert-manager #229

Merged
merged 1 commit into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# UTC Instant

## Why a new class?

During the development of this project, various different time formats came into play. Mostly the 10 minute intervals used by GAEN and the default milliseconds used by the dp3t API. But time handling is hard. So `OffsetDateTimes` at UTC offsets were used, and sometimes `Dates` were needed, and some classes wanted different Objects and and and....

We ended up with a mess of 300 character lines, converting from one object into the other. For this reason we added this class, which gathers all usages and conversions we needed during development. It also should ensure that different people write the same code, by providing _one thing who times 'em all_.

This also allowed us to the name the functions in a concise and natural way. The first example reads more natural than the second one.

```java
if(UTCInstant.of(keyDate, GaenUnit.TenMinutes).isBeforeDateOf(UTCInstant.now().atStartOfDay().plusDays(2)) {

}
```

```java
if(Instant.ofEpochMilli(Duration.of(10, GaenUnit.TenMinutes).toMillis()).isBefore(LocalDate.now().atStartOfDay().plusDays(2).toInstant(ZoneOffset.UTC))) {

}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Configurations

This package holds all possible configurations. For the Swiss-Covid App the cloud-configurations, together with the `MultipleJWTConfig` and the `ActuatorSecurity` are used.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
import org.dpppt.backend.sdk.model.proto.Exposed;
import org.dpppt.backend.sdk.utils.UTCInstant;
import org.dpppt.backend.sdk.ws.security.ValidateRequest;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException;
import org.dpppt.backend.sdk.ws.util.ValidationUtils;
import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException;
import org.springframework.http.CacheControl;
Expand Down Expand Up @@ -114,19 +116,25 @@ public DPPPTController(
+ " + OS-Version",
example = "ch.ubique.android.starsdk;1.0;iOS;13.3")
String userAgent,
@AuthenticationPrincipal Object principal)
throws InvalidDateException {
@AuthenticationPrincipal Object principal) {
var now = UTCInstant.now();
if (!this.validateRequest.isValid(principal)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
long keyDate;

try {
if (!this.validateRequest.isValid(principal)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
keyDate = this.validateRequest.validateKeyDate(now, principal, exposeeRequest);
} catch (WrongScopeException | ClaimIsBeforeOnsetException | InvalidDateException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

if (!validationUtils.isValidBase64Key(exposeeRequest.getKey())) {
return new ResponseEntity<>("No valid base64 key", HttpStatus.BAD_REQUEST);
}
// TODO: should we give that information?
Exposee exposee = new Exposee();
exposee.setKey(exposeeRequest.getKey());
long keyDate = this.validateRequest.getKeyDate(now, principal, exposeeRequest);

exposee.setKeyDate(keyDate);
if (!this.validateRequest.isFakeRequest(principal, exposeeRequest)) {
Expand Down Expand Up @@ -169,8 +177,9 @@ public DPPPTController(
example = "ch.ubique.android.starsdk;1.0;iOS;13.3")
String userAgent,
@AuthenticationPrincipal Object principal)
throws InvalidDateException {
throws InvalidDateException, WrongScopeException, ClaimIsBeforeOnsetException {
var now = UTCInstant.now();

if (!this.validateRequest.isValid(principal)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
Expand All @@ -183,7 +192,7 @@ public DPPPTController(

Exposee exposee = new Exposee();
exposee.setKey(exposedKey.getKey());
long keyDate = this.validateRequest.getKeyDate(now, principal, exposedKey);
long keyDate = this.validateRequest.validateKeyDate(now, principal, exposedKey);

exposee.setKeyDate(keyDate);
exposees.add(exposee);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import org.dpppt.backend.sdk.model.gaen.GaenRequest;
import org.dpppt.backend.sdk.utils.UTCInstant;
import org.dpppt.backend.sdk.ws.security.ValidateRequest;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException;
import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException;
import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature;
import org.dpppt.backend.sdk.ws.util.ValidationUtils;
import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException;
Expand Down Expand Up @@ -63,17 +65,16 @@ public DebugController(
@RequestHeader(value = "User-Agent", required = true) String userAgent,
@RequestHeader(value = "X-Device-Name", required = true) String deviceName,
@AuthenticationPrincipal Object principal)
throws InvalidDateException {
throws InvalidDateException, ClaimIsBeforeOnsetException, WrongScopeException {
var now = UTCInstant.now();
if (!this.validateRequest.isValid(principal)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
this.validateRequest.isValid(principal);

List<GaenKey> nonFakeKeys = new ArrayList<>();
for (var key : gaenRequest.getGaenKeys()) {
if (!validationUtils.isValidBase64Key(key.getKeyData())) {
return new ResponseEntity<>("No valid base64 key", HttpStatus.BAD_REQUEST);
}
this.validateRequest.getKeyDate(now, principal, key);
this.validateRequest.validateKeyDate(now, principal, key);
if (this.validateRequest.isFakeRequest(principal, key)) {
continue;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,13 @@ public GaenController(
@Documentation(description = "JWT token that can be verified by the backend server")
Object principal) {
var now = UTCInstant.now();
if (!this.validateRequest.isValid(principal)) {
boolean valid = false;
try {
valid = this.validateRequest.isValid(principal);
} catch (ValidateRequest.WrongScopeException e) {
logger.error("Got invalid scope: " + e);
}
if (!valid) {
return () -> ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}

Expand Down Expand Up @@ -409,9 +415,9 @@ private boolean hasNegativeRollingPeriod(GaenKey key) {

private boolean hasInvalidKeyDate(UTCInstant now, Object principal, GaenKey key) {
try {
this.validateRequest.getKeyDate(now, principal, key);
} catch (InvalidDateException invalidDate) {
logger.error(invalidDate.getLocalizedMessage());
this.validateRequest.validateKeyDate(now, principal, key);
} catch (InvalidDateException | ValidateRequest.ClaimIsBeforeOnsetException e) {
logger.error(e.getLocalizedMessage());
return true;
}
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Controllers

The `DPPPTController` is deprecated, and will eventually be removed in future versions. The `DebugController` can be used to test various Scenario, together with the callibration app.
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ public boolean isValid(Object authObject) {
}

@Override
public long getKeyDate(UTCInstant now, Object authObject, Object others)
throws InvalidDateException {
public long validateKeyDate(UTCInstant now, Object authObject, Object others)
throws InvalidDateException, ClaimIsBeforeOnsetException {
if (authObject instanceof Jwt) {
Jwt token = (Jwt) authObject;
var jwtKeyDate = UTCInstant.parseDate(token.getClaim("onset"));
if (others instanceof ExposeeRequest) {
ExposeeRequest request = (ExposeeRequest) others;
var requestKeyDate = UTCInstant.ofEpochMillis(request.getKeyDate());
if (!validationUtils.isDateInRange(requestKeyDate, now)
|| requestKeyDate.isBeforeEpochMillisOf(jwtKeyDate)) {
if (!validationUtils.isDateInRange(requestKeyDate, now)) {
throw new InvalidDateException();
} else if (requestKeyDate.isBeforeEpochMillisOf(jwtKeyDate)) {
throw new ClaimIsBeforeOnsetException();
}
jwtKeyDate = UTCInstant.ofEpochMillis(request.getKeyDate());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ public boolean isValid(Object authObject) {
}

@Override
public long getKeyDate(UTCInstant now, Object authObject, Object others)
throws InvalidDateException {
public long validateKeyDate(UTCInstant now, Object authObject, Object others)
throws ClaimIsBeforeOnsetException, InvalidDateException {
if (others instanceof ExposeeRequest) {
ExposeeRequest request = ((ExposeeRequest) others);
var requestKeyDate = new UTCInstant(request.getKeyDate());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Validation and Cryptography
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,30 @@

public interface ValidateRequest {

public boolean isValid(Object authObject);
public boolean isValid(Object authObject) throws WrongScopeException;

// authObject is the Principal, given from Springboot
// others can be any object (currently it is the ExposeeRequest, since we want
// to allow no auth without the jwt profile)
public long getKeyDate(UTCInstant now, Object authObject, Object others)
throws InvalidDateException;
public long validateKeyDate(UTCInstant now, Object authObject, Object others)
throws ClaimIsBeforeOnsetException, InvalidDateException;

public boolean isFakeRequest(Object authObject, Object others);

public class InvalidDateException extends Exception {

private static final long serialVersionUID = 5886601055826066148L;
}

public class ClaimDoesNotMatchKeyDateException extends Exception {
private static final long serialVersionUID = 5886601055826066149L;
}

public class ClaimIsBeforeOnsetException extends Exception {
private static final long serialVersionUID = 5886601055826066150L;
}

public class WrongScopeException extends Exception {
private static final long serialVersionUID = 5886601055826066151L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,30 @@ public JWTValidateRequest(ValidationUtils validationUtils) {
}

@Override
public boolean isValid(Object authObject) {
public boolean isValid(Object authObject) throws WrongScopeException {
if (authObject instanceof Jwt) {
Jwt token = (Jwt) authObject;
return token.containsClaim("scope") && token.getClaim("scope").equals("exposed");
if (Boolean.TRUE.equals(token.containsClaim("scope"))
&& token.getClaim("scope").equals("exposed")) {
return true;
}
throw new WrongScopeException();
}
return false;
}

@Override
public long getKeyDate(UTCInstant now, Object authObject, Object others)
throws InvalidDateException {
public long validateKeyDate(UTCInstant now, Object authObject, Object others)
throws ClaimIsBeforeOnsetException, InvalidDateException {
if (authObject instanceof Jwt) {
Jwt token = (Jwt) authObject;
var jwtKeyDate = UTCInstant.parseDate(token.getClaim("onset"));
if (others instanceof GaenKey) {
GaenKey request = (GaenKey) others;
var keyDate = UTCInstant.of(request.getRollingStartNumber(), GaenUnit.TenMinutes);
if (!validationUtils.isDateInRange(keyDate, now)
|| keyDate.isBeforeEpochMillisOf(jwtKeyDate)) {
if (keyDate.isBeforeEpochMillisOf(jwtKeyDate)) {
throw new ClaimIsBeforeOnsetException();
} else if (!validationUtils.isDateInRange(keyDate, now)) {
throw new InvalidDateException();
}
jwtKeyDate = keyDate;
Expand All @@ -59,7 +64,8 @@ public boolean isFakeRequest(Object authObject, Object others) {
Jwt token = (Jwt) authObject;
GaenKey request = (GaenKey) others;
boolean fake = false;
if (token.containsClaim("fake") && token.getClaimAsString("fake").equals("1")) {
if (Boolean.TRUE.equals(token.containsClaim("fake"))
&& token.getClaimAsString("fake").equals("1")) {
fake = true;
}
if (request.getFake() == 1) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Exposure Notification (Protobuf and Signing)
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

import java.time.Duration;
import java.util.Base64;
import org.dpppt.backend.sdk.model.gaen.GaenKey;
import org.dpppt.backend.sdk.utils.UTCInstant;
import org.springframework.security.oauth2.jwt.Jwt;

/** Offers a set of methods to validate the incoming requests from the mobile devices. */
public class ValidationUtils {
Expand Down Expand Up @@ -63,6 +65,15 @@ public boolean isDateInRange(UTCInstant timestamp, UTCInstant now) {
// Because _now_ has a resolution of 1 millisecond, this precision is acceptable.
return timestamp.isAfterEpochMillisOf(retention) && timestamp.isBeforeEpochMillisOf(now);
}
/**
* Check if the given date is before now - retentionPeriod ... now
*
* @param timestamp to verify
* @return if the date is in the range
*/
public boolean isBeforeRetention(UTCInstant timestamp, UTCInstant now) {
return timestamp.isBeforeDateOf(now.minus(retentionPeriod));
}

/**
* Check if the given timestamp is a valid key date: Must be midnight UTC.
Expand Down Expand Up @@ -90,8 +101,49 @@ public boolean isValidBatchReleaseTime(UTCInstant batchReleaseTime, UTCInstant n
return this.isDateInRange(batchReleaseTime, now);
}

public void validateDelayedKeyDate(UTCInstant now, UTCInstant delayedKeyDate)
throws DelayedKeyDateIsInvalid {
if (delayedKeyDate.isBeforeDateOf(now.getLocalDate().minusDays(1))
|| delayedKeyDate.isAfterDateOf(now.getLocalDate().plusDays(1))) {
throw new DelayedKeyDateIsInvalid();
}
}

public void checkForDelayedKeyDateClaim(Object principal, GaenKey delayedKey)
throws DelayedKeyDateClaimIsWrong {
if (principal instanceof Jwt
&& Boolean.FALSE.equals(((Jwt) principal).containsClaim("delayedKeyDate"))) {
throw new DelayedKeyDateClaimIsWrong();
}
if (principal instanceof Jwt) {
var jwt = (Jwt) principal;
var claimKeyDate = Integer.parseInt(jwt.getClaimAsString("delayedKeyDate"));
if (!delayedKey.getRollingStartNumber().equals(claimKeyDate)) {
throw new DelayedKeyDateClaimIsWrong();
}
}
}

public boolean jwtIsFake(Object principal) {
return principal instanceof Jwt
&& Boolean.TRUE.equals(((Jwt) principal).containsClaim("fake"))
&& ((Jwt) principal).getClaim("fake").equals("1");
}

public class BadBatchReleaseTimeException extends Exception {

private static final long serialVersionUID = 618376703047108588L;
}

public class DelayedKeyDateIsInvalid extends Exception {

/** */
private static final long serialVersionUID = -2667236967819549686L;
}

public class DelayedKeyDateClaimIsWrong extends Exception {

/** */
private static final long serialVersionUID = 4683923905451080793L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import javax.servlet.Filter;
import javax.sql.DataSource;
Expand Down Expand Up @@ -182,8 +181,7 @@ protected String createMaliciousToken(UTCInstant expiresAt) {
.setSubject(
"test-subject" + OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC).toString())
.setExpiration(expiresAt.getDate())
.setIssuedAt(
Date.from(OffsetDateTime.now().withOffsetSameInstant(ZoneOffset.UTC).toInstant()))
.setIssuedAt(UTCInstant.now().getDate())
.compact();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -306,17 +306,17 @@ public void cannotUseKeyDateBeforeOnset() throws Exception {
createToken(
UTCInstant.now().plusMinutes(5),
UTCInstant.now().getLocalDate().format(DateTimeFormatter.ISO_DATE));
MockHttpServletResponse response =
mockMvc
.perform(
post("/v1/exposed")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.header("User-Agent", "MockMVC")
.content(json(exposeeRequest)))
.andExpect(status().is(400))
.andReturn()
.getResponse();

mockMvc
.perform(
post("/v1/exposed")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + token)
.header("User-Agent", "MockMVC")
.content(json(exposeeRequest)))
.andExpect(status().is(400))
.andReturn()
.getResponse();
}

@Test
Expand Down
Loading