Skip to content

Commit dfc56c0

Browse files
Validate authorization proof for pre/post-compute requests (#635)
1 parent 546135d commit dfc56c0

File tree

8 files changed

+332
-43
lines changed

8 files changed

+332
-43
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
77
### New Features
88

99
- Use TEE framework version of dApp to retrieve pre/post-compute properties via SMS endpoint. (#630)
10+
- Validate authorization proof for pre/post-compute requests. (#635)
1011

1112
### Quality
1213

src/main/java/com/iexec/worker/chain/ContributionService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
import com.iexec.common.contribution.Contribution;
2020
import com.iexec.common.replicate.ReplicateStatusCause;
2121
import com.iexec.common.result.ComputedFile;
22-
import com.iexec.common.worker.result.ResultUtils;
2322
import com.iexec.commons.poco.chain.*;
2423
import com.iexec.commons.poco.contract.generated.IexecHubContract;
2524
import com.iexec.commons.poco.task.TaskDescription;
2625
import com.iexec.commons.poco.utils.BytesUtils;
26+
import com.iexec.commons.poco.utils.HashUtils;
2727
import lombok.extern.slf4j.Slf4j;
2828
import org.springframework.stereotype.Service;
2929

@@ -153,8 +153,8 @@ public Contribution getContribution(ComputedFile computedFile) {
153153
}
154154

155155
String resultDigest = computedFile.getResultDigest();
156-
String resultHash = ResultUtils.computeResultHash(chainTaskId, resultDigest);
157-
String resultSeal = ResultUtils.computeResultSeal(workerWalletAddress, chainTaskId, resultDigest);
156+
String resultHash = HashUtils.concatenateAndHash(chainTaskId, resultDigest);
157+
String resultSeal = HashUtils.concatenateAndHash(workerWalletAddress, chainTaskId, resultDigest);
158158
String workerpoolSignature = workerpoolAuthorization.getSignature().getValue();
159159
String enclaveChallenge = workerpoolAuthorization.getEnclaveChallenge();
160160
String enclaveSignature = computedFile.getEnclaveSignature();

src/main/java/com/iexec/worker/chain/WorkerpoolAuthorizationService.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.iexec.common.lifecycle.purge.ExpiringTaskMapFactory;
2020
import com.iexec.common.lifecycle.purge.Purgeable;
2121
import com.iexec.commons.poco.chain.WorkerpoolAuthorization;
22+
import com.iexec.commons.poco.security.Signature;
2223
import com.iexec.commons.poco.utils.BytesUtils;
2324
import com.iexec.commons.poco.utils.HashUtils;
2425
import com.iexec.commons.poco.utils.SignatureUtils;
@@ -29,6 +30,7 @@
2930
import org.springframework.stereotype.Service;
3031

3132
import java.util.Map;
33+
import java.util.NoSuchElementException;
3234

3335

3436
@Slf4j
@@ -77,6 +79,28 @@ WorkerpoolAuthorization getWorkerpoolAuthorization(final String chainTaskId) {
7779
return workerpoolAuthorizations.get(chainTaskId);
7880
}
7981

82+
public boolean isSignedWithEnclaveChallenge(final String chainTaskId, final String authorization) {
83+
final WorkerpoolAuthorization workerpoolAuthorization = workerpoolAuthorizations.get(chainTaskId);
84+
if (workerpoolAuthorization == null) {
85+
throw new NoSuchElementException("Cant get workerpool authorization for chainTaskId: " + chainTaskId);
86+
}
87+
final String challenge = getChallenge(workerpoolAuthorization);
88+
final Signature signature = new Signature(authorization);
89+
90+
return SignatureUtils.isSignatureValid(
91+
BytesUtils.stringToBytes(challenge),
92+
signature,
93+
workerpoolAuthorization.getEnclaveChallenge()
94+
);
95+
}
96+
97+
private String getChallenge(final WorkerpoolAuthorization workerpoolAuthorization) {
98+
return HashUtils.concatenateAndHash(
99+
workerpoolAuthorization.getChainTaskId(),
100+
workerpoolAuthorization.getWorkerWallet()
101+
);
102+
}
103+
80104
/**
81105
* Try and remove workerpool authorization related to given task ID.
82106
*

src/main/java/com/iexec/worker/compute/ComputeController.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2023 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2022-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,13 +19,13 @@
1919

2020
import com.iexec.common.result.ComputedFile;
2121
import com.iexec.common.worker.api.ExitMessage;
22+
import com.iexec.worker.chain.WorkerpoolAuthorizationService;
2223
import com.iexec.worker.result.ResultService;
2324
import org.springframework.http.HttpStatus;
2425
import org.springframework.http.ResponseEntity;
25-
import org.springframework.web.bind.annotation.PathVariable;
26-
import org.springframework.web.bind.annotation.PostMapping;
27-
import org.springframework.web.bind.annotation.RequestBody;
28-
import org.springframework.web.bind.annotation.RestController;
26+
import org.springframework.web.bind.annotation.*;
27+
28+
import java.util.NoSuchElementException;
2929

3030
import static org.springframework.http.ResponseEntity.ok;
3131

@@ -34,26 +34,42 @@ public class ComputeController {
3434

3535
private final ComputeExitCauseService computeStageExitService;
3636
private final ResultService resultService;
37+
private final WorkerpoolAuthorizationService workerpoolAuthorizationService;
3738

38-
public ComputeController(ComputeExitCauseService computeStageExitService,
39-
ResultService resultService) {
39+
public ComputeController(final ComputeExitCauseService computeStageExitService,
40+
final ResultService resultService,
41+
final WorkerpoolAuthorizationService workerpoolAuthorizationService) {
4042
this.computeStageExitService = computeStageExitService;
4143
this.resultService = resultService;
44+
this.workerpoolAuthorizationService = workerpoolAuthorizationService;
4245
}
4346

4447
@PostMapping("/compute/{stage}/{chainTaskId}/exit")
4548
public ResponseEntity<Void> sendExitCauseForGivenComputeStage(
49+
@RequestHeader("Authorization") String authorization,
4650
@PathVariable ComputeStage stage,
4751
@PathVariable String chainTaskId,
4852
@RequestBody ExitMessage exitMessage) {
49-
if (exitMessage.getCause() == null) {
53+
try {
54+
if (!workerpoolAuthorizationService.isSignedWithEnclaveChallenge(chainTaskId, authorization)) {
55+
return ResponseEntity
56+
.status(HttpStatus.UNAUTHORIZED.value())
57+
.build();
58+
}
59+
} catch (NoSuchElementException e) {
60+
return ResponseEntity
61+
.status(HttpStatus.NOT_FOUND.value())
62+
.build();
63+
}
64+
65+
if (exitMessage.cause() == null) {
5066
return ResponseEntity
5167
.status(HttpStatus.BAD_REQUEST.value())
5268
.build();
5369
}
5470
if (!computeStageExitService.setExitCause(stage,
5571
chainTaskId,
56-
exitMessage.getCause())) {
72+
exitMessage.cause())) {
5773
return ResponseEntity
5874
.status(HttpStatus.ALREADY_REPORTED.value())
5975
.build();
@@ -65,8 +81,20 @@ public ResponseEntity<Void> sendExitCauseForGivenComputeStage(
6581
"/iexec_out/{chainTaskId}/computed", //@Deprecated
6682
"/compute/" + ComputeStage.POST_VALUE + "/{chainTaskId}/computed"
6783
})
68-
public ResponseEntity<String> sendComputedFileForTee(@PathVariable String chainTaskId,
84+
public ResponseEntity<String> sendComputedFileForTee(@RequestHeader("Authorization") String authorization,
85+
@PathVariable String chainTaskId,
6986
@RequestBody ComputedFile computedFile) {
87+
try {
88+
if (!workerpoolAuthorizationService.isSignedWithEnclaveChallenge(chainTaskId, authorization)) {
89+
return ResponseEntity
90+
.status(HttpStatus.UNAUTHORIZED.value())
91+
.build();
92+
}
93+
} catch (NoSuchElementException e) {
94+
return ResponseEntity
95+
.status(HttpStatus.NOT_FOUND.value())
96+
.build();
97+
}
7098
if (!chainTaskId.equals(computedFile.getTaskId())) {
7199
return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).build();
72100
}

src/test/java/com/iexec/worker/chain/ContributionServiceTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import com.iexec.common.contribution.Contribution;
2020
import com.iexec.common.result.ComputedFile;
21-
import com.iexec.common.worker.result.ResultUtils;
2221
import com.iexec.commons.poco.chain.*;
2322
import com.iexec.commons.poco.task.TaskDescription;
2423
import com.iexec.commons.poco.utils.BytesUtils;
24+
import com.iexec.commons.poco.utils.HashUtils;
2525
import com.iexec.commons.poco.utils.TestUtils;
2626
import org.junit.jupiter.api.BeforeEach;
2727
import org.junit.jupiter.api.Test;
@@ -298,8 +298,8 @@ void getContribution() {
298298
String chainTaskId = "0x0000000000000000000000000000000000000000000000000000000000000001";
299299
String resultDigest = "0x0000000000000000000000000000000000000000000000000000000000000002";
300300

301-
String resultHash = ResultUtils.computeResultHash(chainTaskId, resultDigest);
302-
String resultSeal = ResultUtils.computeResultSeal(Credentials.create(TestUtils.WORKER_PRIVATE).getAddress(), chainTaskId, resultDigest);
301+
String resultHash = HashUtils.concatenateAndHash(chainTaskId, resultDigest);
302+
String resultSeal = HashUtils.concatenateAndHash(Credentials.create(TestUtils.WORKER_PRIVATE).getAddress(), chainTaskId, resultDigest);
303303

304304
WorkerpoolAuthorization teeWorkerpoolAuth = TestUtils.getTeeWorkerpoolAuth();
305305
when(workerpoolAuthorizationService.getWorkerpoolAuthorization(chainTaskId)).thenReturn(teeWorkerpoolAuth);
@@ -331,8 +331,8 @@ void getContributionWithTee() {
331331
String chainTaskId = "0x0000000000000000000000000000000000000000000000000000000000000002";
332332
String resultDigest = "0x0000000000000000000000000000000000000000000000000000000000000001";
333333

334-
String resultHash = ResultUtils.computeResultHash(chainTaskId, resultDigest);
335-
String resultSeal = ResultUtils.computeResultSeal(Credentials.create(TestUtils.WORKER_PRIVATE).getAddress(), chainTaskId, resultDigest);
334+
String resultHash = HashUtils.concatenateAndHash(chainTaskId, resultDigest);
335+
String resultSeal = HashUtils.concatenateAndHash(Credentials.create(TestUtils.WORKER_PRIVATE).getAddress(), chainTaskId, resultDigest);
336336

337337
WorkerpoolAuthorization teeWorkerpoolAuth = TestUtils.getTeeWorkerpoolAuth();
338338
teeWorkerpoolAuth.setEnclaveChallenge(TestUtils.ENCLAVE_ADDRESS);

src/test/java/com/iexec/worker/chain/WorkerpoolAuthorizationServiceTests.java

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 IEXEC BLOCKCHAIN TECH
2+
* Copyright 2020-2025 IEXEC BLOCKCHAIN TECH
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,16 +19,23 @@
1919
import com.iexec.commons.poco.chain.WorkerpoolAuthorization;
2020
import com.iexec.commons.poco.security.Signature;
2121
import com.iexec.commons.poco.utils.BytesUtils;
22+
import com.iexec.commons.poco.utils.HashUtils;
23+
import com.iexec.commons.poco.utils.SignatureUtils;
2224
import com.iexec.worker.config.SchedulerConfiguration;
2325
import org.junit.jupiter.api.Test;
2426
import org.junit.jupiter.api.extension.ExtendWith;
2527
import org.mockito.InjectMocks;
2628
import org.mockito.Mock;
2729
import org.mockito.junit.jupiter.MockitoExtension;
2830
import org.springframework.test.util.ReflectionTestUtils;
31+
import org.web3j.crypto.Credentials;
32+
import org.web3j.crypto.ECKeyPair;
33+
import org.web3j.crypto.Keys;
2934

35+
import java.util.Arrays;
3036
import java.util.HashMap;
3137
import java.util.Map;
38+
import java.util.NoSuchElementException;
3239

3340
import static org.junit.jupiter.api.Assertions.*;
3441
import static org.mockito.ArgumentMatchers.any;
@@ -37,7 +44,7 @@
3744

3845
@ExtendWith(MockitoExtension.class)
3946
class WorkerpoolAuthorizationServiceTests {
40-
private static final String CHAIN_TASK_ID = "chainTaskId";
47+
private static final String CHAIN_TASK_ID = "0xd94b63fc2d3ec4b96daf84b403bbafdc8c8517e8e2addd51fec0fa4e67801be8";
4148

4249
@Mock
4350
private SchedulerConfiguration schedulerConfiguration;
@@ -125,6 +132,80 @@ void shouldPurgeAllTasksData() {
125132
}
126133
// endregion
127134

135+
// region getChallenge
136+
@Test
137+
void shouldGetChallenge() {
138+
final WorkerpoolAuthorization workerpoolAuthorization = getWorkerpoolAuthorization();
139+
140+
final String expectedChallenge = HashUtils.concatenateAndHash(
141+
workerpoolAuthorization.getChainTaskId(),
142+
workerpoolAuthorization.getWorkerWallet()
143+
);
144+
145+
final String challenge = ReflectionTestUtils.invokeMethod(
146+
workerpoolAuthorizationService,
147+
"getChallenge",
148+
workerpoolAuthorization);
149+
150+
assertEquals(expectedChallenge, challenge);
151+
}
152+
// endregion
153+
154+
// region isSignedWithEnclaveChallenge
155+
@Test
156+
void shouldConfirmSignatureIsSignedWithEnclaveChallenge() throws Exception {
157+
final ECKeyPair workerKeyPair = Keys.createEcKeyPair();
158+
final Credentials workerCredentials = Credentials.create(workerKeyPair);
159+
final String workerWallet = workerCredentials.getAddress();
160+
161+
final ECKeyPair enclaveKeyPair = Keys.createEcKeyPair();
162+
final Credentials enclaveCredentials = Credentials.create(enclaveKeyPair);
163+
final String enclaveWallet = enclaveCredentials.getAddress();
164+
165+
final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization.builder()
166+
.workerWallet(workerWallet)
167+
.chainTaskId(CHAIN_TASK_ID)
168+
.enclaveChallenge(enclaveWallet)
169+
.build();
170+
171+
final Map<String, WorkerpoolAuthorization> workerpoolAuthorizations = new HashMap<>();
172+
workerpoolAuthorizations.put(CHAIN_TASK_ID, workerpoolAuthorization);
173+
ReflectionTestUtils.setField(workerpoolAuthorizationService, "workerpoolAuthorizations", workerpoolAuthorizations);
174+
175+
final String challenge = HashUtils.concatenateAndHash(CHAIN_TASK_ID, workerWallet);
176+
final Signature signature = SignatureUtils.signMessageHashAndGetSignature(challenge, enclaveKeyPair);
177+
final boolean isValid = workerpoolAuthorizationService.isSignedWithEnclaveChallenge(
178+
CHAIN_TASK_ID,
179+
signature.getValue());
180+
181+
assertTrue(isValid);
182+
}
183+
184+
@Test
185+
void shouldRejectSignatureNotSignedWithEnclaveChallenge() {
186+
final WorkerpoolAuthorization workerpoolAuthorization = getWorkerpoolAuthorization();
187+
final Map<String, WorkerpoolAuthorization> workerpoolAuthorizations = new HashMap<>();
188+
workerpoolAuthorizations.put(workerpoolAuthorization.getChainTaskId(), workerpoolAuthorization);
189+
ReflectionTestUtils.setField(workerpoolAuthorizationService, "workerpoolAuthorizations", workerpoolAuthorizations);
190+
191+
final Signature invalidSignature = SignatureUtils.signMessageHashAndGetSignature(
192+
Arrays.toString(BytesUtils.stringToBytes("wrong-challenge")),
193+
workerpoolAuthorization.getWorkerWallet()
194+
);
195+
196+
assertFalse(workerpoolAuthorizationService.isSignedWithEnclaveChallenge(
197+
workerpoolAuthorization.getChainTaskId(),
198+
invalidSignature.toString()));
199+
}
200+
201+
@Test
202+
void shouldThrowExceptionWhenWorkerpoolAuthorizationNotFound() {
203+
assertThrows(NoSuchElementException.class, () ->
204+
workerpoolAuthorizationService.isSignedWithEnclaveChallenge(CHAIN_TASK_ID, "anySignature")
205+
);
206+
}
207+
// endregion
208+
128209
private WorkerpoolAuthorization getWorkerpoolAuthorization() {
129210
final String workerWallet = "0x748e091bf16048cb5103E0E10F9D5a8b7fBDd860";
130211
final String chainTaskId = "0xd94b63fc2d3ec4b96daf84b403bbafdc8c8517e8e2addd51fec0fa4e67801be8";

0 commit comments

Comments
 (0)