diff --git a/.github/workflows/close_issue.yaml b/.github/workflows/close_issue.yaml new file mode 100644 index 00000000..71945d71 --- /dev/null +++ b/.github/workflows/close_issue.yaml @@ -0,0 +1,48 @@ +name: close-issues + +# (c) 2020 by Linus Gasser for C4DT.org +# This action closes issues referenced in the PR in the case the merge +# does not happen on the 'default' branch. +# It searches for the same tags as the original github closers. + +on: + pull_request: + types: [closed] + branches: [develop] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: script + uses: actions/github-script@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (!pr.merged){ + console.log("Don't close issues when PR is not merged"); + return; + } + + const body = pr.body; + const lines = body.split('\n').map((l)=>l.trim()); + const closers = new RegExp( + /(close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)/i); + const issues = lines.filter((l)=>l.match(closers)); + const issue_nbrs = issues.map((i) => i.replace(/.*#/, '')); + issue_nbrs.forEach((i) => { + console.log("Closing issue " + i); + github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: i, + body: `Closed by PR #${pr.number} ${pr.title}` + }); + github.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: i, + state: 'closed' + }); + }); diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..44abcbe1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +Currently eligble versions for patches are the following + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | + + +## Reporting a Vulnerability +Please report (suspected) security vulnerabilities to ubsecurity@ubique.ch. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java index a911cf21..09d2ba67 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java @@ -166,11 +166,13 @@ public byte[] getPayload(Map> groupedBuckets) ZipOutputStream zip = new ZipOutputStream(byteOut); zip.putNextEntry(new ZipEntry("export.bin")); - byte[] exportBin = protoFile.toByteArray(); - zip.write(EXPORT_MAGIC); + byte[] protoFileBytes = protoFile.toByteArray(); + byte[] exportBin = new byte[EXPORT_MAGIC.length + protoFileBytes.length]; + System.arraycopy(EXPORT_MAGIC, 0, exportBin, 0, EXPORT_MAGIC.length); + System.arraycopy(protoFileBytes, 0, exportBin, EXPORT_MAGIC.length, protoFileBytes.length); zip.write(exportBin); zip.closeEntry(); - + var signatureList = getSignatureObject(exportBin); byte[] exportSig = signatureList.toByteArray(); diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java index 0686d1ef..42cd7882 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenControllerTest.java @@ -33,10 +33,8 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Base64; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -65,13 +63,12 @@ import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; -@ActiveProfiles({"actuator-security"}) +@ActiveProfiles({ "actuator-security" }) @SpringBootTest(properties = { "ws.app.jwt.publickey=classpath://generated_pub.pem", - "logging.level.org.springframework.security=DEBUG", "ws.exposedlist.releaseBucketDuration=7200000", "ws.gaen.randomkeysenabled=true", - "ws.monitor.prometheus.user=prometheus", - "ws.monitor.prometheus.password=prometheus", - "management.endpoints.enabled-by-default=true", - "management.endpoints.web.exposure.include=*"}) + "logging.level.org.springframework.security=DEBUG", "ws.exposedlist.releaseBucketDuration=7200000", + "ws.gaen.randomkeysenabled=true", "ws.monitor.prometheus.user=prometheus", + "ws.monitor.prometheus.password=prometheus", "management.endpoints.enabled-by-default=true", + "management.endpoints.web.exposure.include=*" }) @Transactional public class GaenControllerTest extends BaseControllerTest { @Autowired @@ -92,24 +89,25 @@ public void testHello() throws Exception { assertNotNull(response); assertEquals("Hello from DP3T WS", response.getContentAsString()); } + @Test public void testActuatorSecurity() throws Exception { var response = mockMvc.perform(get("/actuator/health")).andExpect(status().is2xxSuccessful()).andReturn() .getResponse(); - response = mockMvc.perform(get("/actuator/loggers")).andExpect(status().is(401)).andReturn() - .getResponse(); - response = mockMvc.perform(get("/actuator/loggers").header("Authorization", "Basic cHJvbWV0aGV1czpwcm9tZXRoZXVz")).andExpect(status().isOk()).andReturn() - .getResponse(); - response = mockMvc.perform(get("/actuator/prometheus")).andExpect(status().is(401)).andReturn() - .getResponse(); - response = mockMvc.perform(get("/actuator/prometheus").header("Authorization", "Basic cHJvbWV0aGV1czpwcm9tZXRoZXVz")).andExpect(status().isOk()).andReturn() - .getResponse(); + response = mockMvc.perform(get("/actuator/loggers")).andExpect(status().is(401)).andReturn().getResponse(); + response = mockMvc + .perform(get("/actuator/loggers").header("Authorization", "Basic cHJvbWV0aGV1czpwcm9tZXRoZXVz")) + .andExpect(status().isOk()).andReturn().getResponse(); + response = mockMvc.perform(get("/actuator/prometheus")).andExpect(status().is(401)).andReturn().getResponse(); + response = mockMvc + .perform(get("/actuator/prometheus").header("Authorization", "Basic cHJvbWV0aGV1czpwcm9tZXRoZXVz")) + .andExpect(status().isOk()).andReturn().getResponse(); } - private void testNKeys(UTCInstant now,int n, boolean shouldSucceed) throws Exception{ + private void testNKeys(UTCInstant now, int n, boolean shouldSucceed) throws Exception { var requestList = new GaenRequest(); var gaenKey1 = new GaenKey(); - gaenKey1.setRollingStartNumber((int)now.atStartOfDay().minusDays(1).get10MinutesSince1970()); + gaenKey1.setRollingStartNumber((int) now.atStartOfDay().minusDays(1).get10MinutesSince1970()); gaenKey1.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes01".getBytes("UTF-8"))); gaenKey1.setRollingPeriod(144); gaenKey1.setFake(0); @@ -123,9 +121,9 @@ private void testNKeys(UTCInstant now,int n, boolean shouldSucceed) throws Excep List exposedKeys = new ArrayList<>(); exposedKeys.add(gaenKey1); exposedKeys.add(gaenKey2); - for (int i = 0; i < n-2; i++) { + for (int i = 0; i < n - 2; i++) { var tmpKey = new GaenKey(); - tmpKey.setRollingStartNumber((int)now.atStartOfDay().get10MinutesSince1970()); + tmpKey.setRollingStartNumber((int) now.atStartOfDay().get10MinutesSince1970()); tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); tmpKey.setRollingPeriod(144); tmpKey.setFake(1); @@ -142,12 +140,10 @@ private void testNKeys(UTCInstant now,int n, boolean shouldSucceed) throws Excep .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29").content(json(requestList))); MvcResult response; - if(shouldSucceed) { - response = requestBuilder.andExpect(request().asyncStarted()) - .andReturn(); + if (shouldSucceed) { + response = requestBuilder.andExpect(request().asyncStarted()).andReturn(); mockMvc.perform(asyncDispatch(response)).andExpect(status().is2xxSuccessful()); - } - else { + } else { response = requestBuilder.andExpect(status().is(400)).andReturn(); return; } @@ -155,15 +151,18 @@ private void testNKeys(UTCInstant now,int n, boolean shouldSucceed) throws Excep .perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken).header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) - .andExpect(status().is(401)).andExpect(request().asyncNotStarted()).andExpect(content().string("")).andReturn(); + .andExpect(status().is(401)).andExpect(request().asyncNotStarted()).andExpect(content().string("")) + .andReturn(); - var result = gaenDataService.getSortedExposedForKeyDate(now.atStartOfDay().minusDays(1).getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration + 1 )*releaseBucketDuration); + var result = gaenDataService.getSortedExposedForKeyDate(now.atStartOfDay().minusDays(1).getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); assertEquals(2, result.size()); - for(var key : result) { + for (var key : result) { assertEquals(Integer.valueOf(144), key.getRollingPeriod()); } - result = gaenDataService.getSortedExposedForKeyDate(now.atStartOfDay().minusDays(1).getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration)*releaseBucketDuration); + result = gaenDataService.getSortedExposedForKeyDate(now.atStartOfDay().minusDays(1).getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration) * releaseBucketDuration); assertEquals(0, result.size()); } @@ -174,7 +173,7 @@ public void testAllKeysWrongButStill200() throws Exception { var requestList = new GaenRequest(); var gaenKey1 = new GaenKey(); - gaenKey1.setRollingStartNumber((int)midnight.minusDays(30).get10MinutesSince1970()); + gaenKey1.setRollingStartNumber((int) midnight.minusDays(30).get10MinutesSince1970()); gaenKey1.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes01".getBytes("UTF-8"))); gaenKey1.setRollingPeriod(0); gaenKey1.setFake(0); @@ -190,7 +189,7 @@ public void testAllKeysWrongButStill200() throws Exception { exposedKeys.add(gaenKey2); for (int i = 0; i < 12; i++) { var tmpKey = new GaenKey(); - tmpKey.setRollingStartNumber((int)midnight.plusDays(10).get10MinutesSince1970()); + tmpKey.setRollingStartNumber((int) midnight.plusDays(10).get10MinutesSince1970()); tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); tmpKey.setRollingPeriod(144); tmpKey.setFake(0); @@ -212,40 +211,45 @@ public void testAllKeysWrongButStill200() throws Exception { .perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + jwtToken).header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(requestList))) - .andExpect(status().is(401)).andExpect(request().asyncNotStarted()).andExpect(content().string("")).andReturn(); + .andExpect(status().is(401)).andExpect(request().asyncNotStarted()).andExpect(content().string("")) + .andReturn(); - var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(1).getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration + 1 )*releaseBucketDuration); - //all keys are in compatible + var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(1).getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + // all keys are in compatible assertEquals(0, result.size()); } @Transactional public void testMultipleKeyUpload() throws Exception { - testNKeys(UTCInstant.now(),14, true); + testNKeys(UTCInstant.now(), 14, true); } + @Test @Transactional public void testCanUploadMoreThan14Keys() throws Exception { - testNKeys(UTCInstant.now(),30, true); + testNKeys(UTCInstant.now(), 30, true); } + @Test @Transactional public void testCannotUploadMoreThan30Keys() throws Exception { - testNKeys(UTCInstant.now(),31,false); - testNKeys(UTCInstant.now(),100,false); - testNKeys(UTCInstant.now(),1000,false); + testNKeys(UTCInstant.now(), 31, false); + testNKeys(UTCInstant.now(), 100, false); + testNKeys(UTCInstant.now(), 1000, false); } + private Map headers = Map.of("X-Content-Type-Options", "nosniff", "X-Frame-Options", "DENY", + "X-Xss-Protection", "1; mode=block"); - private Map headers= Map.of("X-Content-Type-Options","nosniff", "X-Frame-Options", "DENY", "X-Xss-Protection", "1; mode=block"); @Test public void testSecurityHeaders() throws Exception { MockHttpServletResponse response = mockMvc.perform(get("/v1")).andExpect(status().is2xxSuccessful()).andReturn() - .getResponse(); - for(var header : headers.keySet()) { + .getResponse(); + for (var header : headers.keySet()) { assertTrue(response.containsHeader(header)); assertEquals(headers.get(header), response.getHeader(header)); - } + } var now = UTCInstant.now(); var midnight = UTCInstant.today(); response = mockMvc @@ -257,7 +261,7 @@ public void testSecurityHeaders() throws Exception { for(var header : headers.keySet()) { assertTrue(response.containsHeader(header)); assertEquals(headers.get(header), response.getHeader(header)); - } + } } @Test @@ -266,13 +270,13 @@ public void testUploadWithNegativeRollingPeriodFails() throws Exception { var midnight = now.atStartOfDay(); var requestList = new GaenRequest(); var gaenKey1 = new GaenKey(); - gaenKey1.setRollingStartNumber((int)midnight.get10MinutesSince1970()); + gaenKey1.setRollingStartNumber((int) midnight.get10MinutesSince1970()); gaenKey1.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); gaenKey1.setRollingPeriod(-1); gaenKey1.setFake(0); gaenKey1.setTransmissionRiskLevel(0); var gaenKey2 = new GaenKey(); - gaenKey2.setRollingStartNumber((int)midnight.minusDays(1).get10MinutesSince1970()); + gaenKey2.setRollingStartNumber((int) midnight.minusDays(1).get10MinutesSince1970()); gaenKey2.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); gaenKey2.setRollingPeriod(-5); gaenKey2.setFake(0); @@ -291,7 +295,7 @@ public void testUploadWithNegativeRollingPeriodFails() throws Exception { exposedKeys.add(tmpKey); } requestList.setGaenKeys(exposedKeys); - var duration = (int)midnight.plusDays(1).get10MinutesSince1970(); + var duration = (int) midnight.plusDays(1).get10MinutesSince1970(); requestList.setDelayedKeyDate((int) duration); gaenKey1.setFake(0); String token = createToken(now.plusMinutes(5)); @@ -302,11 +306,13 @@ public void testUploadWithNegativeRollingPeriodFails() throws Exception { mockMvc.perform(asyncDispatch(response)).andExpect(status().is(200)); - var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(1).getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration + 1 )*releaseBucketDuration); - //all keys are in compatible + var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(1).getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + // all keys are invalid assertEquals(0, result.size()); - result = gaenDataService.getSortedExposedForKeyDate(midnight.getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration + 1 )*releaseBucketDuration); - //all keys are in compatible + result = gaenDataService.getSortedExposedForKeyDate(midnight.getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); + // all keys are invalid assertEquals(0, result.size()); } @@ -681,15 +687,15 @@ public void keyDateNotOlderThan21Days() throws Exception { } exposeeRequest.setGaenKeys(keys); - String token = createToken(now.plusMinutes(5), - "2020-01-01"); + String token = createToken(now.plusMinutes(5), "2020-01-01"); MvcResult response = mockMvc .perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token).header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) .andExpect(request().asyncStarted()).andExpect(status().is(200)).andReturn(); - var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(22).getTimestamp(),null, (now.getTimestamp() / releaseBucketDuration + 1 )*releaseBucketDuration); + var result = gaenDataService.getSortedExposedForKeyDate(midnight.minusDays(22).getTimestamp(), null, + (now.getTimestamp() / releaseBucketDuration + 1) * releaseBucketDuration); assertEquals(0, result.size()); } @@ -704,16 +710,14 @@ public void cannotUseTokenWithWrongScope() throws Exception { GaenKey key = new GaenKey(); key.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); key.setRollingPeriod(144); - key.setRollingStartNumber( - (int) now.get10MinutesSince1970()); + key.setRollingStartNumber((int) now.get10MinutesSince1970()); key.setTransmissionRiskLevel(1); key.setFake(1); List keys = new ArrayList<>(); keys.add(key); for (int i = 0; i < 13; i++) { var tmpKey = new GaenKey(); - tmpKey.setRollingStartNumber( - (int) now.get10MinutesSince1970()); + tmpKey.setRollingStartNumber((int) now.get10MinutesSince1970()); tmpKey.setKeyData(Base64.getEncoder().encodeToString("testKey32Bytes--".getBytes("UTF-8"))); tmpKey.setRollingPeriod(144); tmpKey.setFake(1); @@ -722,8 +726,7 @@ public void cannotUseTokenWithWrongScope() throws Exception { } exposeeRequest.setGaenKeys(keys); - String token = createTokenWithScope(now.plusMinutes(5), - "not-exposed"); + String token = createTokenWithScope(now.plusMinutes(5), "not-exposed"); MvcResult response = mockMvc .perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) @@ -737,7 +740,7 @@ public void cannotUseTokenWithWrongScope() throws Exception { .perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) .header("Authorization", "Bearer " + token).header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29") .content(json(exposeeRequest))) - .andExpect(request().asyncNotStarted()).andExpect(status().is(401)).andExpect(content().string("")) + .andExpect(request().asyncNotStarted()).andExpect(status().is(401)).andExpect(content().string("")) .andReturn(); } @@ -844,11 +847,14 @@ public void delayedKeyDateBoundaryCheck() throws Exception { tmpKey.setTransmissionRiskLevel(0); keys.add(tmpKey); } - Map tests = Map.of(-2, false, + + Map tests = Map.of( + -2, false, -1, true, 0, true, 1, true, 2, false); + for (Map.Entry t : tests.entrySet()) { Integer offset = t.getKey(); Boolean pass = t.getValue(); @@ -866,13 +872,13 @@ public void delayedKeyDateBoundaryCheck() throws Exception { MvcResult responseAsync = mockMvc.perform(post("/v1/gaen/exposed") .contentType(MediaType.APPLICATION_JSON).header("Authorization", "Bearer " + token) .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29").content(json(exposeeRequest))).andExpect(status().is(400)).andReturn(); - + } } } @Test - public void testTokenValiditySurpassesMaxJwtValidity() throws Exception{ + public void testTokenValiditySurpassesMaxJwtValidity() throws Exception { var now = UTCInstant.now(); var midnight = now.atStartOfDay(); @@ -904,22 +910,64 @@ public void testDebugController() throws Exception { var now = UTCInstant.now(); var midnight = now.atStartOfDay(); - insertNKeysPerDayInIntervalWithDebugFlag(14, - midnight.minusDays(4), - midnight, midnight.minusDays(1), true); + // insert two times 10 keys per day for the last 14 days, with different + // received at. In total: 280 keys + insertNKeysPerDay(midnight, 14, 10, midnight.minusDays(1), true); + insertNKeysPerDay(midnight, 14, 10, midnight.minusHours(12), true); + + // Request keys which have been received in the last day, must be 280 in total. + // This is the debug controller, which returns keys based on the 'received at', + // not based on the key date. So this request should return all keys with + // 'received at' of the last day. - insertNKeysPerDayInIntervalWithDebugFlag(14, - midnight.minusDays(4), - midnight, midnight.minusHours(12), true); MockHttpServletResponse response = mockMvc .perform(get("/v1/debug/exposed/" + midnight.getTimestamp()) .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) .andExpect(status().is2xxSuccessful()).andReturn().getResponse(); - - verifyZipInZipResponse(response, 0); + verifyZipInZipResponse(response, 280, 144); } + @Test + @Transactional + public void zipContainsFiles() throws Exception { + var now = UTCInstant.now(); + var clock = Clock.offset(Clock.systemUTC(), now.getDuration(now.atStartOfDay().plusHours(12))); + UTCInstant.setClock(clock); + now = UTCInstant.now(); + var midnight = now.atStartOfDay(); + + // Insert two times 5 keys per day for the last 14 days. the second batch has a + // different 'received at' timestamp. (+12 hours compared to the first) + insertNKeysPerDay(midnight, 14, 5, midnight.minusDays(1), false); + insertNKeysPerDay(midnight, 14, 5, midnight.minusHours(12), false); + + // request the keys with key date 8 days ago. no publish until. + MockHttpServletResponse response = mockMvc + .perform( + get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()).header("User-Agent", "MockMVC")) + .andExpect(status().is2xxSuccessful()).andReturn().getResponse(); + + Long publishedUntil = Long.parseLong(response.getHeader("X-PUBLISHED-UNTIL")); + assertTrue(publishedUntil < now.getTimestamp(), "Published until must be in the past"); + + // must contain 20 keys: 5 from the first insert, 5 from the second insert and + // 10 random keys + verifyZipResponse(response, 20, 144); + + // request again the keys with date date 8 days ago. with publish until, so that + // we only get the second batch. + var bucketAfterSecondRelease = midnight.minusHours(12); + + MockHttpServletResponse responseWithPublishedAfter = mockMvc + .perform(get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()).header("User-Agent", "MockMVC") + .param("publishedafter", Long.toString(bucketAfterSecondRelease.getTimestamp()))) + .andExpect(status().is2xxSuccessful()).andReturn().getResponse(); + + // must contain 15 keys: 5 from the second insert and 10 random keys + verifyZipResponse(responseWithPublishedAfter, 15, 144); + UTCInstant.resetClock(); + } @Test @Transactional(transactionManager = "testTransactionManager") @@ -934,15 +982,13 @@ public void testNonEmptyResponseAnd304() throws Exception { .andExpect(status().isOk()).andReturn().getResponse(); verifyZipResponse(response, 10, 144); } - + @Test @Transactional(transactionManager = "testTransactionManager") public void testTodayWeDontHaveKeys() throws Exception { var midnight = UTCInstant.today(); MockHttpServletResponse response = mockMvc - .perform(get("/v1/gaen/exposed/" - + midnight.getTimestamp()) - .header("User-Agent", "MockMVC")) + .perform(get("/v1/gaen/exposed/" + midnight.getTimestamp()).header("User-Agent", "MockMVC")) .andExpect(status().is(204)).andReturn().getResponse(); } @@ -952,13 +998,9 @@ public void testEtag() throws Exception { var now = UTCInstant.now(); var midnight = now.atStartOfDay(); - insertNKeysPerDayInInterval(14, - midnight.minusDays(4), - midnight, midnight.minusDays(1)); + insertNKeysPerDay(midnight, 14, 10, midnight.minusDays(1), false); + insertNKeysPerDay(midnight, 14, 10, midnight.minusHours(12), false); - insertNKeysPerDayInInterval(14, - midnight.minusDays(4), - midnight, midnight.minusHours(12)); // request the keys with date date 1 day ago. no publish until. MockHttpServletResponse response = mockMvc .perform(get("/v1/gaen/exposed/" @@ -980,10 +1022,9 @@ public void testEtag() throws Exception { assertTrue(publishedUntil < System.currentTimeMillis(), "Published until must be in the past"); assertEquals(expectedEtag, response.getHeader("etag")); - insertNKeysPerDayInInterval(14, - midnight.minusDays(4), - midnight, midnight.minusHours(12)); - response = mockMvc + insertNKeysPerDay(midnight, 14, 10, midnight.minusHours(12), false); + + response = mockMvc .perform(get("/v1/gaen/exposed/" + midnight.minusDays(8).getTimestamp()) .header("User-Agent", "ch.admin.bag.dp3t.dev;1.0.7;1595591959493;Android;29")) @@ -993,34 +1034,48 @@ public void testEtag() throws Exception { assertTrue(publishedUntil < System.currentTimeMillis(), "Published until must be in the past"); assertNotEquals(expectedEtag, response.getHeader("etag")); } - + @Test public void testMalciousTokenFails() throws Exception { var requestList = new GaenRequest(); List exposedKeys = new ArrayList(); requestList.setGaenKeys(exposedKeys); String token = createMaliciousToken(UTCInstant.now().plusMinutes(5)); - MvcResult response = mockMvc.perform(post("/v1/gaen/exposed") - .contentType(MediaType.APPLICATION_JSON).header("Authorization", "Bearer " + token) - .header("User-Agent", "MockMVC").content(json(requestList))).andExpect(request().asyncNotStarted()).andExpect(status().is(401)).andReturn(); + MvcResult response = mockMvc.perform(post("/v1/gaen/exposed").contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + token).header("User-Agent", "MockMVC").content(json(requestList))) + .andExpect(request().asyncNotStarted()).andExpect(status().is(401)).andReturn(); String authenticateError = response.getResponse().getHeader("www-authenticate"); assertTrue(authenticateError.contains("Unsigned Claims JWTs are not supported.")); } - private void verifyZipInZipResponse(MockHttpServletResponse response, int expectKeyCount) throws Exception { + /** + * Verifies a zip in zip response, that each inner zip is again valid. + */ + private void verifyZipInZipResponse(MockHttpServletResponse response, int expectKeyCount, int expectedRollingPeriod) + throws Exception { ByteArrayInputStream baisOuter = new ByteArrayInputStream(response.getContentAsByteArray()); ZipInputStream zipOuter = new ZipInputStream(baisOuter); ZipEntry entry = zipOuter.getNextEntry(); - while(entry != null) { + while (entry != null) { + ZipInputStream zipInner = new ZipInputStream(new ByteArrayInputStream(zipOuter.readAllBytes())); + verifyKeyZip(zipInner, expectKeyCount, expectedRollingPeriod); entry = zipOuter.getNextEntry(); } } + /** + * Verifies a zip response, checks if keys and signature is correct. + */ private void verifyZipResponse(MockHttpServletResponse response, int expectKeyCount, int expectedRollingPeriod) throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { - ByteArrayInputStream baisOuter = new ByteArrayInputStream(response.getContentAsByteArray()); - ZipInputStream zipOuter = new ZipInputStream(baisOuter); - ZipEntry entry = zipOuter.getNextEntry(); + ByteArrayInputStream baisZip = new ByteArrayInputStream(response.getContentAsByteArray()); + ZipInputStream keyZipInputstream = new ZipInputStream(baisZip); + verifyKeyZip(keyZipInputstream, expectKeyCount, expectedRollingPeriod); + } + + private void verifyKeyZip(ZipInputStream keyZipInputstream, int expectKeyCount, int expectedRollingPeriod) + throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { + ZipEntry entry = keyZipInputstream.getNextEntry(); boolean foundData = false; boolean foundSignature = false; @@ -1031,15 +1086,15 @@ private void verifyZipResponse(MockHttpServletResponse response, int expectKeyCo while (entry != null) { if (entry.getName().equals("export.bin")) { foundData = true; - exportBin = zipOuter.readAllBytes(); - keyProto = new byte[exportBin.length-16]; + exportBin = keyZipInputstream.readAllBytes(); + keyProto = new byte[exportBin.length - 16]; System.arraycopy(exportBin, 16, keyProto, 0, keyProto.length); } if (entry.getName().equals("export.sig")) { foundSignature = true; - signatureProto = zipOuter.readAllBytes(); + signatureProto = keyZipInputstream.readAllBytes(); } - entry = zipOuter.getNextEntry(); + entry = keyZipInputstream.getNextEntry(); } assertTrue(foundData, "export.bin not found in zip"); @@ -1047,7 +1102,7 @@ private void verifyZipResponse(MockHttpServletResponse response, int expectKeyCo TEKSignatureList list = TemporaryExposureKeyFormat.TEKSignatureList.parseFrom(signatureProto); TemporaryExposureKeyExport export = TemporaryExposureKeyFormat.TemporaryExposureKeyExport.parseFrom(keyProto); - for(var key : export.getKeysList()) { + for (var key : export.getKeysList()) { assertEquals(expectedRollingPeriod, key.getRollingPeriod()); } var sig = list.getSignatures(0); @@ -1061,50 +1116,40 @@ private void verifyZipResponse(MockHttpServletResponse response, int expectKeyCo assertEquals(expectKeyCount, export.getKeysCount()); } - private void insertNKeysPerDayInIntervalWithDebugFlag(int n, UTCInstant start, UTCInstant end, UTCInstant receivedAt, boolean debug) throws Exception { - var current = start; - Map rollingToCount = new HashMap<>(); - while (current.isBeforeEpochMillisOf(end)) { + /** + * Creates keysPerDay for every day: lastDay, lastDay-1, ..., lastDay - daysBack + * + 1 + * + * @param lastDay of the created keys + * @param daysBack of the key creation, counted including the lastDay + * @param keysPerDay that will be created for every day + * @param receivedAt as sent to the DB + * @param debug if true, inserts the keys in the debug table. + */ + private void insertNKeysPerDay(UTCInstant lastDay, int daysBack, int keysPerDay, UTCInstant receivedAt, + boolean debug) { + SecureRandom random = new SecureRandom(); + for (int d = 0; d < daysBack; d++) { + var currentKeyDate = lastDay.minusDays(d); + int currentRollingStartNumber = (int) currentKeyDate.get10MinutesSince1970(); List keys = new ArrayList<>(); - SecureRandom random = new SecureRandom(); - int lastRolling = (int)start.get10MinutesSince1970(); - for (int i = 0; i < n; i++) { + for (int n = 0; n < keysPerDay; n++) { GaenKey key = new GaenKey(); byte[] keyBytes = new byte[16]; random.nextBytes(keyBytes); key.setKeyData(Base64.getEncoder().encodeToString(keyBytes)); key.setRollingPeriod(144); - logger.info("Rolling Start number: " + lastRolling); - key.setRollingStartNumber(lastRolling); + key.setRollingStartNumber(currentRollingStartNumber); key.setTransmissionRiskLevel(1); key.setFake(0); keys.add(key); - - Integer count = rollingToCount.get(lastRolling); - if (count == null) { - count = 0; - } - count = count + 1; - rollingToCount.put(lastRolling, count); - - lastRolling -= Duration.ofDays(1).dividedBy(Duration.ofMinutes(10)); - } - if(debug) { + if (debug) { testGaenDataService.upsertExposeesDebug(keys, receivedAt); } else { testGaenDataService.upsertExposees(keys, receivedAt); } - current = current.plusDays(1); - } - for (Entry entry: rollingToCount.entrySet()) { - logger.info("Rolling start number: " + entry.getKey() + " -> count: " + entry.getValue() + " (received at: " + receivedAt.toString() + ")"); } } - private void insertNKeysPerDayInInterval(int n, UTCInstant start, UTCInstant end, UTCInstant receivedAt) - throws Exception { - insertNKeysPerDayInIntervalWithDebugFlag(n, start, end, receivedAt, false); - } - -} \ No newline at end of file +}