diff --git a/.gitignore b/.gitignore index ac541123..8dee8fdf 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,9 @@ build /.nb-gradle/ /bin/ +/iexec-sms-library/bin .vscode/ ### iExec ### -src/main/resources/iexec-sms-aes.key -src/main/resources/boot/sms-palaemon-conf.yml +src/test/resources/iexec-sms-aes.key diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bd033e..5ae8b638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. +## [[8.0.0]](https://github.com/iExecBlockchainComputing/iexec-sms/releases/tag/v8.0.0) 2023-03-06 + +### New Features +* Support SMS in enclave for Scone TEE tasks. +* Support Gramine framework for TEE tasks. +* Add `GET /up` client method in iexec-sms-library. +* Return a same `SmsClient` from the `SmsClientProvider` of iexec-sms-library when calling a same SMS URL. +* Add iExec banner at startup. +* Show application version on banner. +### Bug Fixes +* Remove TLS context on server. +* Remove `GET /secrets` endpoints. +* Remove non-TEE workflow. +* Remove enclave entrypoints from Gramine sessions since already present in manifests of applications. +* Update Scone transformation parameters to enable health checks in SMS in enclave. +### Quality +* Refactor secret model. +* Improve code quality. +### Dependency Upgrades +* Upgrade to Spring Boot 2.6.14. +* Upgrade to Gradle 7.6. +* Upgrade OkHttp to 4.9.0. +* Upgrade to Java 11.0.16 patch. +* Upgrade to `iexec-common` 7.0.0. +* Upgrade to `jenkins-library` 2.4.0. + ## [[7.3.0]](https://github.com/iExecBlockchainComputing/iexec-sms/releases/tag/v7.3.0) 2023-01-18 * Add endpoint to allow health checks. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..ce868e59 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM openjdk:11.0.16-jre-slim + +ARG jar + +RUN test -n "$jar" + +RUN apt-get update \ + && apt-get install -y curl \ + && rm -rf /var/lib/apt/lists/* + +COPY $jar /app/iexec-sms.jar + +COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 + +ENTRYPOINT [ "/bin/sh", "-c", "java -jar /app/iexec-sms.jar" ] diff --git a/Jenkinsfile b/Jenkinsfile index 281186b5..9c5216ad 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,13 +1,26 @@ -@Library('global-jenkins-library@2.2.3') _ +@Library('global-jenkins-library@2.4.0') _ + +String repositoryName = 'iexec-sms' + +buildInfo = getBuildInfo() + buildJavaProject( - buildInfo: getBuildInfo(), + buildInfo: buildInfo, integrationTestsEnvVars: [], shouldPublishJars: true, shouldPublishDockerImages: true, - dockerfileDir: 'build/resources/main', - dockerfileFilename: 'Dockerfile.untrusted', + dockerfileDir: '.', buildContext: '.', preDevelopVisibility: 'iex.ec', developVisibility: 'iex.ec', preProductionVisibility: 'docker.io', productionVisibility: 'docker.io') + +sconeBuildUnlocked( + nativeImage: "docker-regis.iex.ec/$repositoryName:$buildInfo.imageTag", + imageName: repositoryName, + imageTag: buildInfo.imageTag, + sconifyArgsPath: './docker/sconify.args', + sconifyImage: 'sconecuratedimages/iexec-sconify-image', + sconifyVersion: '5.7.0-wal' +) diff --git a/README.md b/README.md index e72d47d0..772fcba5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ ## Overview -The iExec Secret Management Service (SMS) stores user secrets and provisions them to authorized Trusted Execution Environment (TEE) applications running on the iExec network. +The _iExec Secret Management Service_ (SMS) stores user secrets and provisions them to authorized Trusted Execution Environment (TEE) applications running on the iExec network. + +Two TEE frameworks for TEE tasks are supported on the iExec platform: + +* Scone +* Gramine ### Details @@ -16,58 +21,71 @@ The iExec Secret Management Service (SMS) stores user secrets and provisions the ## Configuration -The iExec Secret Management Service is available as an OCI image on [Docker Hub](https://hub.docker.com/r/iexechub/iexec-sms/tags). +The _iExec Secret Management Service_ is available as an OCI image on [Docker Hub](https://hub.docker.com/r/iexechub/iexec-sms/tags). + +A single _iExec Secret Management Service_ instance supports a single TEE framework. +To support both Scone and Gramine TEE tasks, two instances of _iExec SMS_ must be configured. -To run properly, the iExec Secret Management Service requires: +To run properly, the _iExec Secret Management Service_ requires: * A blockchain node. iExec smart contracts must be deployed on the blockchain network. -* A Scontain Configuration and Attestation Service (CAS) to provision secrets to remote enclaves. -* Valid OCI images configurations for pre-compute and post-compute stages of TEE tasks executions. -* A valid OCI image configuration of a Scontain Local Attestation Service (LAS). - This service will be deployed by an iExec Worker to compute TEE tasks. +* Valid OCI images configurations for pre-compute and post-compute stages of TEE tasks executions. Exposed images depend on the type of TEE framework supported. +* A Secret Provisioner instance, in charge of provisioning secrets to remote enclaves. Each TEE framework requires its own type of Secret Provisioner. + * for Scone TEE tasks: + * a Scontain _Configuration and Attestation Service_ (CAS). + * a valid OCI image configuration of a Scontain _Local Attestation Service_ (LAS). This service will be deployed by an iExec Worker to compute TEE tasks. + * for Gramine TEE tasks: + * an _iExec Secret Provisioner Service_ (_iExec SPS_) instance. -The iExec SMS can be started locally for development purpose. +The _iExec Secret Management Service_ can be started locally for development purpose. It is not advised to use an instance with such configuration in production. -You can configure the SMS with the following properties: +To support: +* Scone TEE tasks, set `IEXEC_SMS_TEE_RUNTIME_FRAMEWORK=scone`, then configure the SMS with properties of all following tables. +* Gramine TEE tasks, set `IEXEC_SMS_TEE_RUNTIME_FRAMEWORK=gramine`, then configure the SMS with properties of following table. + +### Environment variables (Scone or Gramine TEE framework) + +| Environment variable | Description | Type | Default Scone-configuration value | Default Gramine-configuration value | +| --- | --- | --- | --- | --- | +| `IEXEC_SMS_TEE_RUNTIME_FRAMEWORK` | Define which TEE framework this _iExec SMS_ supports. | `scone` or `gramine` | | | +| `IEXEC_SMS_PORT` | Server HTTP port. | Positive integer | `13300` | `13300` | +| `IEXEC_SMS_H2_URL` | JDBC URL of the database. | URL | `jdbc:h2:file:/tmp/h2/sms-h2` | `jdbc:h2:file:/tmp/h2/sms-h2` | +| `IEXEC_SMS_H2_CONSOLE` | Whether to enable the H2 console. | Boolean | `false` | `false` | +| `IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH` | Path to the key created and used to encrypt secrets. | String | `src/main/resources/iexec-sms-aes.key` | `src/main/resources/iexec-sms-aes.key` | +| `IEXEC_CHAIN_ID` | Chain ID of the blockchain network to connect. | Positive integer | `17` | `17` | +| `IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS` | URL to connect to the blockchain node. | URL | `http://localhost:8545` | `http://localhost:8545` | +| `IEXEC_HUB_ADDRESS` | Proxy contract address to interact with the iExec on-chain protocol. | String | `0xBF6B2B07e47326B7c8bfCb4A5460bef9f0Fd2002` | `0xBF6B2B07e47326B7c8bfCb4A5460bef9f0Fd2002` | +| `IEXEC_GAS_PRICE_MULTIPLIER` | Transactions will be sent with `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER`. | Float | `1.0` | `1.0` | +| `IEXEC_GAS_PRICE_CAP` | In Wei, will be used for transactions if `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER > IEXEC_GAS_PRICE_CAP`. | Integer | `22000000000` | `22000000000` | +| `IEXEC_IS_SIDECHAIN` | Define if iExec on-chain protocol is built on top of token (`false`) or native currency (`true`). | Boolean | `false` | `false` | +| `IEXEC_SMS_DISPLAY_DEBUG_SESSION` | Whether to display TEE enclaves sessions configuration in SMS logs. | Boolean | `false` | `false` | +| `IEXEC_SECRET_PROVISIONER_WEB_HOSTNAME` | Secret provisioner server host for session management. Used to post sessions of secrets. | String | `localhost` | `localhost` | +| `IEXEC_SECRET_PROVISIONER_WEB_PORT` | Secret provisioner server port for session management. | Positive integer | `8081` | `8080` | +| `IEXEC_SECRET_PROVISIONER_ENCLAVE_HOSTNAME` | Secret provisioner server host for retrieving secrets from attested enclaves. Typically used by workers to execute TEE tasks. | Positive integer | `localhost` | `localhost` | +| `IEXEC_SECRET_PROVISIONER_ENCLAVE_PORT`| Secret provisioner server port for retrieving secrets from attested enclaves. | Positive integer | `18765` | `4433` | +| `IEXEC_TEE_WORKER_PRE_COMPUTE_IMAGE` | TEE enabled OCI image name for worker pre-compute stage of TEE tasks. | String | | | +| `IEXEC_TEE_WORKER_PRE_COMPUTE_FINGERPRINT` | Fingerprint (aka mrenclave) of the TEE enabled worker pre-compute image. | String | | | +| `IEXEC_TEE_WORKER_PRE_COMPUTE_HEAP_SIZE_GB` | Required heap size for a worker pre-compute enclave (in Giga Bytes). | Positive integer | `3` | `3` | +| `IEXEC_TEE_WORKER_PRE_COMPUTE_ENTRYPOINT` | Command executed when starting a container from the TEE enabled worker pre-compute image. | String | `java -jar /app/app.jar` | `/bin/bash /apploader.sh` | +| `IEXEC_TEE_WORKER_POST_COMPUTE_IMAGE` | TEE enabled OCI image name for worker post-compute stage of TEE tasks. | String | | | +| `IEXEC_TEE_WORKER_POST_COMPUTE_FINGERPRINT` | Fingerprint (aka mrenclave) of the TEE enabled worker post-compute image. | String | | | +| `IEXEC_TEE_WORKER_POST_COMPUTE_HEAP_SIZE_GB` | Required heap size for a worker post-compute enclave (in Giga Bytes). | Positive integer | `3` | `3` | +| `IEXEC_TEE_WORKER_POST_COMPUTE_ENTRYPOINT` | Command executed when starting a container from the TEE enabled worker post-compute image. | String | `java -jar /app/app.jar` | `/bin/bash /apploader.sh` | + +### Scone specific environment variables -| Environment variable | Description | Type | Default value | +| Environment variable | Description | Type | Default Scone-configuration value | | --- | --- | --- | --- | -| IEXEC_SMS_PORT | Server HTTPS port. | Positive integer | `15443` | -| IEXEC_SMS_HTTP_ENABLED | Whether to start an http context when starting the SMS. | Boolean | `true` | -| IEXEC_SMS_HTTP_PORT | Server HTTP port. | Positive integer | `13300` | -| IEXEC_SMS_SSL_KEYSTORE | Path to the key store that holds the SSL certificate. | String | `src/main/resources/ssl-keystore-dev.p12` | -| IEXEC_SMS_SSL_KEYSTORE_PASSWORD | Password used to access the key store. | String | `whatever` | -| IEXEC_SMS_SSL_KEYSTORE_TYPE | Type of the key store. | Positive integer | `PKCS12` | -| IEXEC_SMS_SSL_KEYSTORE_ALIAS | Alias that identifies the key in the key store. | String | `iexec-core` | -| IEXEC_SMS_H2_URL | JDBC URL of the database. | URL | `jdbc:h2:file:/tmp/h2/sms-h2` | -| IEXEC_SMS_H2_CONSOLE | Whether to enable the H2 console. | Boolean | `false` | -| IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH | Path to the key created and used to encrypt secrets. | String | `src/main/resources/iexec-sms-aes.key` | -| IEXEC_CHAIN_ID | Chain ID of the blockchain network to connect. | Positive integer | `17` | -| IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS | URL to connect to the blockchain node. | URL | `http://localhost:8545` | -| IEXEC_HUB_ADDRESS | Proxy contract address to interact with the iExec on-chain protocol. | String | `0xBF6B2B07e47326B7c8bfCb4A5460bef9f0Fd2002` | -| IEXEC_GAS_PRICE_MULTIPLIER | Transactions will be sent with `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER`. | Float | `1.0` | -| IEXEC_GAS_PRICE_CAP | In Wei, will be used for transactions if `networkGasPrice * IEXEC_GAS_PRICE_MULTIPLIER > IEXEC_GAS_PRICE_CAP`. | Integer | `22000000000` | -| IEXEC_IS_SIDECHAIN | Define if iExec on-chain protocol is built on top of token (`false`) or native currency (`true`). | Boolean | `false` | -| IEXEC_SCONE_CAS_HOST | CAS service host. | String | `localhost` | -| IEXEC_SCONE_CAS_PORT | Server port of the CAS client API (session management). | Positive integer | `8081` | -| IEXEC_SCONE_CAS_PUBLIC_HOST | Server port of the CAS enclave API (remote attestation). Typically used by workers to execute TEE tasks. | Positive integer | `localhost` | -| IEXEC_SCONE_CAS_ENCLAVE_PORT | Scontain CAS service enclave port, used from worker host to attest applications running within enclaves. | Positive integer | `18765` | -| IEXEC_PALAEMON_TEMPLATE | Path to the template file used to generate configurations of TEE enclave sessions. | String | `src/main/resources/palaemonTemplate.vm` | -| IEXEC_SCONE_TOLERATED_INSECURE_OPTIONS | List of hardware or software Scone vulnerabilities to ignore. | String | | -| IEXEC_IGNORED_SGX_ADVISORIES | List of hardware or software Intel vulnerabilities to ignore. | String | | -| IEXEC_SMS_IMAGE_LAS_IMAGE | Scontain LAS OCI image to be used by workers to execute TEE tasks. LAS performs local attestation which creates a quote that CAS can verify. | String | | -| IEXEC_TEE_WORKER_PRE_COMPUTE_IMAGE | TEE enabled OCI image name for worker pre-compute stage of TEE tasks. | String | | -| IEXEC_TEE_WORKER_PRE_COMPUTE_FINGERPRINT | Fingerprint (aka mrenclave) of the TEE enabled worker pre-compute image. | String | | -| IEXEC_TEE_WORKER_PRE_COMPUTE_HEAP_SIZE_GB | Required heap size for a worker pre-compute enclave (in Giga Bytes). | Positive integer | `4` | -| IEXEC_TEE_WORKER_PRE_COMPUTE_ENTRYPOINT | Command executed when starting a container from the TEE enabled worker pre-compute image. | String | `java -jar /app/app.jar` | -| IEXEC_TEE_WORKER_POST_COMPUTE_IMAGE | TEE enabled OCI image name for worker post-compute stage of TEE tasks. | String | | -| IEXEC_TEE_WORKER_POST_COMPUTE_FINGERPRINT | Fingerprint (aka mrenclave) of the TEE enabled worker post-compute image. | String | | -| IEXEC_TEE_WORKER_POST_COMPUTE_HEAP_SIZE_GB | Required heap size for a worker post-compute enclave (in Giga Bytes). | Positive integer | `4` | -| IEXEC_TEE_WORKER_POST_COMPUTE_ENTRYPOINT | Command executed when starting a container from the TEE enabled worker post-compute image. | String | `java -jar /app/app.jar` | -| IEXEC_SMS_DISPLAY_DEBUG_SESSION | Whether to display TEE enclaves sessions configuration in SMS logs. | Boolean | `false` | +| `IEXEC_SMS_SSL_KEYSTORE` | Path to the key store that holds the SSL certificate. | String | `src/main/resources/ssl-keystore-dev.p12` | +| `IEXEC_SMS_SSL_KEYSTORE_PASSWORD` | Password used to access the key store. | String | `whatever` | +| `IEXEC_SMS_SSL_KEYSTORE_TYPE` | Type of the key store. | Positive integer | `PKCS12` | +| `IEXEC_SMS_SSL_KEYSTORE_ALIAS` | Alias that identifies the key in the key store. | String | `iexec-core` | +| `IEXEC_SCONE_TOLERATED_INSECURE_OPTIONS` | List of hardware or software Scone vulnerabilities to ignore. | String | | +| `IEXEC_IGNORED_SGX_ADVISORIES` | List of hardware or software Intel vulnerabilities to ignore. | String | | +| `IEXEC_SMS_IMAGE_LAS_IMAGE` | Scontain LAS OCI image to be used by workers to execute TEE tasks. LAS performs local attestation which creates a quote that CAS can verify. | String | | ## Health checks -A health endpoint (`/actuator/health`) is enabled by default and can be accessed on the **IEXEC_SMS_HTTP_PORT**. +A health endpoint (`/actuator/health`) is enabled by default and can be accessed on the `IEXEC_SMS_PORT`. This endpoint allows to define health checks in an orchestrator or a [compose file](https://github.com/compose-spec/compose-spec/blob/master/spec.md#healthcheck). -No default strategy has been implemented in the [Dockerfile](src/main/resources/Dockerfile.untrusted) at the moment. +No default strategy has been implemented in the [Dockerfile](Dockerfile) at the moment. diff --git a/build.gradle b/build.gradle index 288f0e43..da654a1f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,17 +1,14 @@ -import org.apache.tools.ant.filters.ReplaceTokens - plugins { id 'java' - id 'io.freefair.lombok' version '5.3.0' - id 'org.springframework.boot' version '2.6.2' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'io.freefair.lombok' version '6.6.1' + id 'org.springframework.boot' version '2.6.14' + id 'io.spring.dependency-management' version '1.1.0' id 'jacoco' id 'org.sonarqube' version '3.3' id 'maven-publish' } ext { - springCloudVersion = '2021.0.0' openFeignVersion = '11.7' } @@ -21,8 +18,6 @@ if (!project.hasProperty('gitBranch')) { allprojects { group = 'com.iexec.sms' - sourceCompatibility = 11 - targetCompatibility = 11 if (gitBranch != 'main' && gitBranch != 'master' && !(gitBranch ==~ '(release|hotfix|support)/.*')) { version += '-NEXT-SNAPSHOT' } @@ -30,17 +25,22 @@ allprojects { mavenLocal() mavenCentral() maven { - url "https://docker-regis-adm.iex.ec/repository/maven-public/" + url 'https://docker-regis-adm.iex.ec/repository/maven-public/' credentials { username nexusUser password nexusPassword } } maven { - url "https://nexus.intra.iex.ec/repository/maven-public/" + url 'https://nexus.intra.iex.ec/repository/maven-public/' } maven { - url "https://jitpack.io" + url 'https://jitpack.io' + } + } + java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) } } } @@ -50,23 +50,16 @@ configurations { integrationTestRuntimeOnly.extendsFrom runtimeOnly } -dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } -} - dependencies { // iexec implementation "com.iexec.common:iexec-common:$iexecCommonVersion" implementation project(':iexec-sms-library') // spring - implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation "org.springframework.boot:spring-boot-starter-web" - implementation "org.springframework.cloud:spring-cloud-starter-openfeign" - implementation "org.springframework.retry:spring-retry" + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.retry:spring-retry' // H2 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2:1.4.200' @@ -75,11 +68,13 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-ui:1.6.3' //ssl - implementation 'org.apache.httpcomponents:httpclient:4.5.9' + implementation 'org.apache.httpcomponents:httpclient' // Web3j issues, see core build.gradle - implementation 'com.squareup.okhttp3:okhttp:4.3.1' - implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.3.50' + // NoSuchMethodError: 'okhttp3.RequestBody okhttp3.RequestBody.create(java.lang.String, okhttp3.MediaType)' + // Spring Boot dependencies BOM enforces okhttp3 3.14.9 in 2.6.X + // It is required to define the dependency version required by web3j until migration to at least Spring Boot 2.7.X + implementation 'com.squareup.okhttp3:okhttp:4.9.0' // Web3j issue: https://github.com/web3j/web3j/issues/1180 // velocity for templating implementation 'org.apache.velocity:velocity-engine-core:2.0' @@ -100,6 +95,11 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter:1.16.0' testImplementation 'org.testcontainers:testcontainers:1.16.0' testImplementation 'org.testcontainers:mongodb:1.16.0' + + // feign + implementation "io.github.openfeign:feign-core:$openFeignVersion" + implementation "io.github.openfeign:feign-jackson:$openFeignVersion" + implementation "io.github.openfeign:feign-slf4j:$openFeignVersion" } sourceSets { @@ -117,6 +117,13 @@ springBoot { buildInfo() } +tasks.named("bootJar") { + manifest { + attributes("Implementation-Title": "iExec Secret Management Service", + "Implementation-Version": project.version) + } +} + test { useJUnitPlatform() } @@ -131,12 +138,12 @@ task itest(type:Test) { } jacoco { - toolVersion = "0.8.7" + toolVersion = '0.8.7' } // sonarqube code coverage requires jacoco XML report jacocoTestReport { reports { - xml.enabled true + xml.required = true } } tasks.test.finalizedBy tasks.jacocoTestReport @@ -145,7 +152,7 @@ tasks.sonarqube.dependsOn tasks.jacocoTestReport publishing { publications { maven(MavenPublication) { - artifact bootJar + artifact tasks.named("bootJar") from components.java } } @@ -168,7 +175,7 @@ task buildImage(type: Exec) { group 'Build' description 'Builds an OCI image from a Dockerfile.' dependsOn bootJar - commandLine ("sh", "-c", "docker build -f build/resources/main/Dockerfile.untrusted --build-arg jar=$jarPathForOCI" + commandLine ('sh', '-c', "docker build --build-arg jar=$jarPathForOCI" + " -t $ociImageName:$gitShortCommit . && docker tag $ociImageName:$gitShortCommit $ociImageName:dev") standardOutput = new ByteArrayOutputStream() @@ -178,57 +185,11 @@ task buildImage(type: Exec) { } } -task buildTrustedImage(type: Exec) { - group 'Build' - description 'Builds a trusted OCI image from a trusted Dockerfile.' - dependsOn bootJar - commandLine ("sh", "-c", "docker image build -f build/resources/main/Dockerfile --build-arg jar=$jarPathForOCI" - + " -t $ociImageName:$gitShortCommit-trusted --no-cache . && docker tag $ociImageName:$gitShortCommit-trusted $ociImageName:dev-trusted") -} - -task templatePalaemon { - String smsMrEnclave - String smsFspfKey - String smsFspfTag - - /* - * Docker build will produce such logs: "RUN .. $MRENCLAVE ... abcdef" - * We need to extract the 2nd occurence 'abcdef' - */ - ext.extractValueBetweenTags = { s, tag -> - String beginTag = "<" + tag + ">" - String endTag = "" - return s.substring(s.indexOf(beginTag, s.indexOf(beginTag) + 2) + beginTag.length(), s.indexOf(endTag, s.indexOf(endTag) + 2)) - } - - doFirst { - String logs = buildImage.output() - smsMrEnclave = extractValueBetweenTags(logs, "MRENCLAVE") - smsFspfKey = extractValueBetweenTags(logs, "FSPF_KEY") - smsFspfTag = extractValueBetweenTags(logs, "FSPF_TAG") - } - - doLast{ - copy { - // delete old one - delete 'src/main/resources/boot/sms-palaemon-conf.yml' - // use and copy template to the new location - from 'src/main/resources/sms-palaemon-conf.yml.template' - into 'src/main/resources/boot/' - - rename { String fileName -> - fileName.replace('.template', '') - } - // replace tokens in the template file - filter(ReplaceTokens, tokens: [ - IEXEC_SMS_PALAEMON_SERVICE_NAME: "${rootProject.name}".toString(), - IEXEC_SMS_MRENCLAVE: smsMrEnclave, - IEXEC_SMS_FSPF_KEY: smsFspfKey, - IEXEC_SMS_FSPF_TAG: smsFspfTag, - ]) - - } - } +task buildSconeImage(type: Exec) { + group "Build" + description "Build an OCI image compatible with scontain TEE framework" + dependsOn buildImage + commandLine "docker/sconify.sh" + environment "IMG_FROM", "$ociImageName:dev" + environment "IMG_TO", "$ociImageName-unlocked:dev" } -//templatePalaemon.dependsOn buildImage -//buildImage.finalizedBy templatePalaemon diff --git a/docker/sconify.args b/docker/sconify.args new file mode 100644 index 00000000..a5e40f5d --- /dev/null +++ b/docker/sconify.args @@ -0,0 +1,12 @@ +--verbose \ +--name=iexec-sms \ +--base=${IMG_FROM} \ +--from=${IMG_FROM} \ +--to=${IMG_TO} \ +--binary=/usr/local/openjdk-11/bin/java \ +--heap="8G" \ +--stack="8M" \ +--binary-fs \ +--fs-dir=/app \ +--host-path=/etc/hosts \ +--host-path=/etc/resolv.conf \ diff --git a/docker/sconify.sh b/docker/sconify.sh new file mode 100755 index 00000000..9a67af3c --- /dev/null +++ b/docker/sconify.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# IMG_FROM=iexechub/iexec-sms:7.1.0 IMG_TO=iexechub/iexec-sms:7.1.0-debug ./sconify.sh +cd $(dirname $0) + +SCONE_IMG_NAME=sconecuratedimages/iexec-sconify-image +SCONE_IMG_VERSION=5.7.0-wal + +IMG_TO=${IMG_TO}-sconify-${SCONE_IMG_VERSION}-debug + +ARGS=$(sed -e "s'\${IMG_FROM}'${IMG_FROM}'" -e "s'\${IMG_TO}'${IMG_TO}'" sconify.args) +echo $ARGS + +SCONE_IMAGE="registry.scontain.com:5050/${SCONE_IMG_NAME}:${SCONE_IMG_VERSION}" + +/bin/bash -c "docker run -t --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + ${SCONE_IMAGE} \ + sconify_iexec \ + --cli=${SCONE_IMAGE} \ + --crosscompiler=${SCONE_IMAGE} \ + $ARGS" + +echo +docker run --rm -e SCONE_HASH=1 $IMG_TO diff --git a/gradle.properties b/gradle.properties index 8c2b2899..e15dd102 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -version=7.3.0 -iexecCommonVersion=6.0.0 +version=8.0.0 +iexecCommonVersion=7.0.0 nexusUser nexusPassword diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf0..943f0cbf 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8cf6eb5a..f398c33c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 8e25e6c1..65dcd68d 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,84 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a14..6689b85b 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,10 +25,14 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/iexec-sms-library/build.gradle b/iexec-sms-library/build.gradle index 80b9cdc6..00f55e4d 100644 --- a/iexec-sms-library/build.gradle +++ b/iexec-sms-library/build.gradle @@ -1,12 +1,15 @@ plugins { id 'java-library' + id 'io.freefair.lombok' id 'jacoco' id 'maven-publish' } dependencies { implementation "com.iexec.common:iexec-common:$iexecCommonVersion" + testImplementation "org.assertj:assertj-core:3.22.0" testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' + testImplementation 'org.mockito:mockito-junit-jupiter:4.7.0' } java { @@ -24,7 +27,7 @@ jacoco { // sonarqube code coverage requires jacoco XML report jacocoTestReport { reports { - xml.enabled true + xml.required = true } } tasks.test.finalizedBy tasks.jacocoTestReport diff --git a/iexec-sms-library/lombok.config b/iexec-sms-library/lombok.config new file mode 100644 index 00000000..189c0bef --- /dev/null +++ b/iexec-sms-library/lombok.config @@ -0,0 +1,3 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java index d4c8f147..11d8341a 100644 --- a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClient.java @@ -17,9 +17,9 @@ package com.iexec.sms.api; import com.iexec.common.chain.WorkerpoolAuthorization; -import com.iexec.common.sms.secret.SmsSecretResponse; -import com.iexec.common.tee.TeeWorkflowSharedConfiguration; +import com.iexec.common.tee.TeeFramework; import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.api.config.TeeServicesProperties; import feign.Headers; import feign.Param; import feign.RequestLine; @@ -34,6 +34,10 @@ */ public interface SmsClient { + @RequestLine("GET /up") + String isUp(); + + // region Secrets @RequestLine("POST /apps/{appAddress}/secrets/1") @Headers("Authorization: {authorization}") ApiResponseBody> addAppDeveloperAppComputeSecret( @@ -49,9 +53,6 @@ ApiResponseBody> isAppDeveloperAppComputeSecretPresent( @Param("secretIndex") String secretIndex ); - @RequestLine("GET /cas/url") - String getSconeCasUrl(); - @RequestLine("POST /requesters/{requesterAddress}/secrets/{secretKey}") @Headers("Authorization: {authorization}") ApiResponseBody> addRequesterAppComputeSecret( @@ -67,6 +68,12 @@ ApiResponseBody> isRequesterAppComputeSecretPresent( @Param("secretKey") String secretKey ); + @RequestLine("HEAD /secrets/web2?ownerAddress={ownerAddress}&secretName={secretName}") + void isWeb2SecretSet( + @Param("ownerAddress") String ownerAddress, + @Param("secretName") String secretName + ); + @RequestLine("POST /secrets/web2?ownerAddress={ownerAddress}&secretName={secretName}") @Headers("Authorization: {authorization}") String setWeb2Secret( @@ -76,6 +83,20 @@ String setWeb2Secret( String secretValue ); + @RequestLine("PUT /secrets/web2?ownerAddress={ownerAddress}&secretName={secretName}") + @Headers("Authorization: {authorization}") + String updateWeb2Secret( + @Param("authorization") String authorization, + @Param("ownerAddress") String ownerAddress, + @Param("secretName") String secretName, + String secretValue + ); + + @RequestLine("HEAD /secrets/web3?secretAddress={secretAddress}") + void isWeb3SecretSet( + @Param("secretAddress") String secretAddress + ); + @RequestLine("POST /secrets/web3?secretAddress={secretAddress}") @Headers("Authorization: {authorization}") String setWeb3Secret( @@ -83,25 +104,23 @@ String setWeb3Secret( @Param("secretAddress") String secretAddress, String secretValue ); + // endregion + // region TEE @RequestLine("POST /tee/challenges/{chainTaskId}") String generateTeeChallenge(@Param("chainTaskId") String chainTaskId); @RequestLine("POST /tee/sessions") @Headers("Authorization: {authorization}") - ApiResponseBody generateTeeSession( + ApiResponseBody generateTeeSession( @Param("authorization") String authorization, WorkerpoolAuthorization workerpoolAuthorization ); - @RequestLine("GET /tee/workflow/config") - TeeWorkflowSharedConfiguration getTeeWorkflowConfiguration(); - - @RequestLine("POST /untee/secrets") - @Headers("Authorization: {authorization}") - SmsSecretResponse getUnTeeSecrets( - @Param("authorization") String authorization, - WorkerpoolAuthorization workerpoolAuthorization - ); + @RequestLine("GET /tee/framework") + TeeFramework getTeeFramework(); + @RequestLine("GET /tee/properties/{teeFramework}") + T getTeeServicesProperties(@Param("teeFramework") TeeFramework teeFramework); + // endregion } diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientCreationException.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientCreationException.java new file mode 100644 index 00000000..68a8ca29 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientCreationException.java @@ -0,0 +1,7 @@ +package com.iexec.sms.api; + +public class SmsClientCreationException extends RuntimeException { + public SmsClientCreationException(String message) { + super(message); + } +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientProvider.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientProvider.java new file mode 100644 index 00000000..af10880d --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/SmsClientProvider.java @@ -0,0 +1,39 @@ +package com.iexec.sms.api; + +import feign.Logger; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages the {@link SmsClient}, providing an easy way to access SMS + * and avoiding the need to create a new {@link SmsClient} instance each time. + */ +public class SmsClientProvider { + private final Map urlToSmsClient = new HashMap<>(); + + private final Logger.Level loggerLevel; + + public SmsClientProvider() { + this(Logger.Level.BASIC); + } + + public SmsClientProvider(Logger.Level loggerLevel) { + this.loggerLevel = loggerLevel; + } + + /** + * Retrieves an SMS client for the specified SMS URL: + *
    + *
  • If this SMS has already been accessed, returns the already-constructed {@link SmsClient};
  • + *
  • Otherwise, constructs, stores and returns a new {@link SmsClient}.
  • + *
+ * + * @param smsUrl URL of the SMS. + * @return An instance of {@link SmsClient} pointing to the specified SMS. + */ + public SmsClient getSmsClient(String smsUrl) { + return urlToSmsClient.computeIfAbsent(smsUrl, url -> SmsClientBuilder.getInstance(loggerLevel, url)); + } + +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java index 77f001c1..61c6a57e 100644 --- a/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationError.java @@ -47,15 +47,17 @@ public enum TeeSessionGenerationError { // endregion // region Secure session generation - SECURE_SESSION_CAS_CALL_FAILED, + SECURE_SESSION_STORAGE_CALL_FAILED, SECURE_SESSION_GENERATION_FAILED, + SECURE_SESSION_NO_TEE_PROVIDER, + SECURE_SESSION_UNKNOWN_TEE_PROVIDER, // endregion // region Miscellaneous GET_TASK_DESCRIPTION_FAILED, NO_SESSION_REQUEST, NO_TASK_DESCRIPTION, - GET_SESSION_YML_FAILED, + GET_SESSION_FAILED, UNKNOWN_ISSUE // endregion diff --git a/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationResponse.java similarity index 69% rename from iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java rename to iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationResponse.java index 3cd6a2e2..123099ba 100644 --- a/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientTest.java +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/TeeSessionGenerationResponse.java @@ -16,15 +16,18 @@ package com.iexec.sms.api; -import feign.Logger; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; -public class SmsClientTest { +@ToString +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class TeeSessionGenerationResponse { - @Test - void instantiationTest() { - Assertions.assertNotNull(SmsClientBuilder.getInstance(Logger.Level.FULL, "localhost")); - } + private String sessionId; + private String secretProvisioningUrl; } diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/config/GramineServicesProperties.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/GramineServicesProperties.java new file mode 100644 index 00000000..85e17089 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/GramineServicesProperties.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.api.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iexec.common.tee.TeeFramework; +import lombok.Getter; + +@Getter +public class GramineServicesProperties extends TeeServicesProperties { + + @JsonCreator + public GramineServicesProperties(@JsonProperty("preComputeProperties") TeeAppProperties preComputeProperties, + @JsonProperty("postComputeProperties") TeeAppProperties postComputeProperties) { + super(TeeFramework.GRAMINE, preComputeProperties, postComputeProperties); + } +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/config/SconeServicesProperties.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/SconeServicesProperties.java new file mode 100644 index 00000000..887e0e1d --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/SconeServicesProperties.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.api.config; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.iexec.common.tee.TeeFramework; +import lombok.Getter; + +@Getter +public class SconeServicesProperties extends TeeServicesProperties { + private final String lasImage; + + @JsonCreator + public SconeServicesProperties(@JsonProperty("preComputeProperties") TeeAppProperties preComputeProperties, + @JsonProperty("postComputeProperties") TeeAppProperties postComputeProperties, + @JsonProperty("lasImage") String lasImage) { + super(TeeFramework.SCONE, preComputeProperties, postComputeProperties); + this.lasImage = lasImage; + } +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeAppProperties.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeAppProperties.java new file mode 100644 index 00000000..cfe0a9b4 --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeAppProperties.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.api.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TeeAppProperties { + private String image; + private String fingerprint; + private String entrypoint; + /** + * Represents the app heap size, in bytes. + */ + private long heapSizeInBytes; +} diff --git a/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeServicesProperties.java b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeServicesProperties.java new file mode 100644 index 00000000..9c50746d --- /dev/null +++ b/iexec-sms-library/src/main/java/com/iexec/sms/api/config/TeeServicesProperties.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.api.config; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.iexec.common.tee.TeeFramework; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "teeFramework", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(name = "SCONE", value = SconeServicesProperties.class), + @JsonSubTypes.Type(name = "GRAMINE", value = GramineServicesProperties.class) +}) +// TODO upgrade to sealed class in Java 17 +public abstract class TeeServicesProperties { + private final TeeFramework teeFramework; + private final TeeAppProperties preComputeProperties; + private final TeeAppProperties postComputeProperties; +} diff --git a/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientBuilderTests.java b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientBuilderTests.java new file mode 100644 index 00000000..d0a2969f --- /dev/null +++ b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientBuilderTests.java @@ -0,0 +1,16 @@ +package com.iexec.sms.api; + +import feign.Logger; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SmsClientBuilderTests { + + // region getInstance + @Test + void instantiationTest() { + assertNotNull(SmsClientBuilder.getInstance(Logger.Level.FULL, "localhost")); + } + // endregion +} \ No newline at end of file diff --git a/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientProviderTests.java b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientProviderTests.java new file mode 100644 index 00000000..ed794a10 --- /dev/null +++ b/iexec-sms-library/src/test/java/com/iexec/sms/api/SmsClientProviderTests.java @@ -0,0 +1,35 @@ +package com.iexec.sms.api; + +import feign.Logger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; + +class SmsClientProviderTests { + private static final String SMS_URL_1 = "smsUrl1"; + private static final String SMS_URL_2 = "smsUrl2"; + private SmsClientProvider smsClientProvider; + + @BeforeEach + void init() { + MockitoAnnotations.openMocks(this); + smsClientProvider = new SmsClientProvider(Logger.Level.NONE); + } + + // region getSmsClient + @Test + void shouldGetSmsClientForUrl() { + // Get same SMS client + SmsClient smsClient1a = smsClientProvider.getSmsClient(SMS_URL_1); + assertNotNull(smsClient1a); + // Get same SMS client on same URL + SmsClient smsClient1b = smsClientProvider.getSmsClient(SMS_URL_1); + assertEquals(smsClient1a, smsClient1b); + // Get different SMS clients + SmsClient smsClient2b = smsClientProvider.getSmsClient(SMS_URL_2); + assertNotEquals(smsClient1a, smsClient2b); + } + // endregion +} \ No newline at end of file diff --git a/iexec-sms-library/src/test/java/com/iexec/sms/api/config/TeeServicesPropertiesTests.java b/iexec-sms-library/src/test/java/com/iexec/sms/api/config/TeeServicesPropertiesTests.java new file mode 100644 index 00000000..89a3fad1 --- /dev/null +++ b/iexec-sms-library/src/test/java/com/iexec/sms/api/config/TeeServicesPropertiesTests.java @@ -0,0 +1,33 @@ +package com.iexec.sms.api.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class TeeServicesPropertiesTests { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + void shouldSerializeAndDeserialize() throws JsonProcessingException { + List teeProperties = new ArrayList<>(); + TeeAppProperties sconePreCompute = new TeeAppProperties("scone-pre-compute", "scone-pre-compute-fingerprint", "scone-pre-compute-entrypoint", 1L); + TeeAppProperties sconePostCompute = new TeeAppProperties("scone-post-compute", "scone-post-compute-fingerprint", "scone-post-compute-entrypoint", 1L); + teeProperties.add(new SconeServicesProperties(sconePreCompute, sconePostCompute, "lasImage")); + + TeeAppProperties graminePreCompute = new TeeAppProperties("gramine-pre-compute", "gramine-pre-compute-fingerprint", "gramine-pre-compute-entrypoint", 1L); + TeeAppProperties graminePostCompute = new TeeAppProperties("gramine-post-compute", "gramine-post-compute-fingerprint", "gramine-post-compute-entrypoint", 1L); + teeProperties.add(new GramineServicesProperties(graminePreCompute, graminePostCompute)); + + String jsonString = mapper.writeValueAsString(teeProperties); + + List deserializedProperties = mapper.readValue(jsonString, new TypeReference<>() {}); + assertThat(deserializedProperties).usingRecursiveComparison().isEqualTo(teeProperties); + } +} diff --git a/src/itest/java/com/iexec/sms/CommonTestSetup.java b/src/itest/java/com/iexec/sms/CommonTestSetup.java index ea333f8c..494288f9 100644 --- a/src/itest/java/com/iexec/sms/CommonTestSetup.java +++ b/src/itest/java/com/iexec/sms/CommonTestSetup.java @@ -16,27 +16,14 @@ package com.iexec.sms; -import com.iexec.sms.blockchain.IexecHubService; -import com.iexec.sms.blockchain.Web3jService; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.TestPropertySource; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) -@TestPropertySource(properties = {"spring.config.location = classpath:/application.yml, classpath:/application-test.yml"}) public abstract class CommonTestSetup { @LocalServerPort protected int randomServerPort; - - // region Following beans are mocked as they use the blockchain - @MockBean - protected IexecHubService iexecHubService; - - @MockBean - protected Web3jService web3jService; - // endregion } diff --git a/src/itest/java/com/iexec/sms/MockChainConfiguration.java b/src/itest/java/com/iexec/sms/MockChainConfiguration.java new file mode 100644 index 00000000..5467c3fb --- /dev/null +++ b/src/itest/java/com/iexec/sms/MockChainConfiguration.java @@ -0,0 +1,20 @@ +package com.iexec.sms; + +import com.iexec.sms.blockchain.IexecHubService; +import com.iexec.sms.blockchain.Web3jService; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import static com.iexec.sms.MockChainConfiguration.MOCK_CHAIN_PROFILE; + +@Configuration +@Profile(MOCK_CHAIN_PROFILE) +public class MockChainConfiguration { + public static final String MOCK_CHAIN_PROFILE = "mock-chain"; + + @MockBean + protected IexecHubService iexecHubService; + @MockBean + protected Web3jService web3jService; +} diff --git a/src/itest/java/com/iexec/sms/MockTeeConfiguration.java b/src/itest/java/com/iexec/sms/MockTeeConfiguration.java new file mode 100644 index 00000000..604eb6a3 --- /dev/null +++ b/src/itest/java/com/iexec/sms/MockTeeConfiguration.java @@ -0,0 +1,28 @@ +package com.iexec.sms; + +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.api.config.TeeServicesProperties; +import com.iexec.sms.tee.session.generic.TeeSessionHandler; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +import static com.iexec.sms.MockTeeConfiguration.MOCK_TEE_PROFILE; + +@Configuration +@Profile(MOCK_TEE_PROFILE) +public class MockTeeConfiguration { + public static final String MOCK_TEE_PROFILE = "mock-tee"; + + @MockBean + private TeeSessionHandler teeSessionHandler; + @MockBean + private TeeServicesProperties teeServicesProperties; + @MockBean + @Qualifier("preComputeProperties") + private TeeAppProperties preComputeProperties; + @MockBean + @Qualifier("postComputeProperties") + private TeeAppProperties postComputeProperties; +} diff --git a/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java b/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java index 92a806c6..92372783 100644 --- a/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java +++ b/src/itest/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretIntegrationTests.java @@ -21,6 +21,7 @@ import com.iexec.sms.CommonTestSetup; import com.iexec.sms.api.SmsClient; import com.iexec.sms.api.SmsClientBuilder; +import com.iexec.sms.blockchain.IexecHubService; import com.iexec.sms.encryption.EncryptionService; import feign.FeignException; import feign.Logger; @@ -35,6 +36,7 @@ import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; import org.web3j.crypto.Hash; import java.util.Date; @@ -44,10 +46,13 @@ import java.util.stream.Collectors; import static com.iexec.common.utils.SignatureUtils.signMessageHashAndGetSignature; +import static com.iexec.sms.MockChainConfiguration.MOCK_CHAIN_PROFILE; +import static com.iexec.sms.MockTeeConfiguration.MOCK_TEE_PROFILE; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @Slf4j +@ActiveProfiles({MOCK_TEE_PROFILE, MOCK_CHAIN_PROFILE, "test"}) public class TeeTaskComputeSecretIntegrationTests extends CommonTestSetup { private static final String APP_ADDRESS = "0xabcd1339ec7e762e639f4887e2bfe5ee8023e23e"; private static final String UPPER_CASE_APP_ADDRESS = "0xABCD1339EC7E762E639F4887E2BFE5EE8023E23E"; @@ -66,6 +71,9 @@ public class TeeTaskComputeSecretIntegrationTests extends CommonTestSetup { @Autowired private TeeTaskComputeSecretRepository repository; + @Autowired + private IexecHubService iexecHubService; + /* * Generate random ASCII from seed for re-testability. * See also {@link org.apache.commons.lang3.RandomStringUtils#randomAscii(int)} @@ -83,7 +91,7 @@ private static String generateRandomAscii(int count) { } @BeforeEach - private void setUp() { + public void setUp() { apiClient = SmsClientBuilder.getInstance(Logger.Level.FULL, "http://localhost:" + randomServerPort); final Ownable appContract = mock(Ownable.class); when(appContract.getContractAddress()).thenReturn(APP_ADDRESS); @@ -137,9 +145,8 @@ void shouldAddNewComputeSecrets() { Assertions.fail("An app developer secret was expected but none has been retrieved."); return; } - Assertions.assertThat(appDeveloperSecret.get().getId()).isNotBlank(); - Assertions.assertThat(appDeveloperSecret.get().getOnChainObjectAddress()).isEqualToIgnoringCase(appAddress); - Assertions.assertThat(appDeveloperSecret.get().getKey()).isEqualTo(appDeveloperSecretIndex); + Assertions.assertThat(appDeveloperSecret.get().getHeader().getOnChainObjectAddress()).isEqualToIgnoringCase(appAddress); + Assertions.assertThat(appDeveloperSecret.get().getHeader().getKey()).isEqualTo(appDeveloperSecretIndex); Assertions.assertThat(appDeveloperSecret.get().getValue()).isNotEqualTo(secretValue); Assertions.assertThat(appDeveloperSecret.get().getValue()).isEqualTo(encryptionService.encrypt(secretValue)); @@ -160,9 +167,8 @@ void shouldAddNewComputeSecrets() { Assertions.fail("An app requester secret was expected but none has been retrieved."); return; } - Assertions.assertThat(requesterSecret.get().getId()).isNotBlank(); - Assertions.assertThat(requesterSecret.get().getOnChainObjectAddress()).isEqualToIgnoringCase(""); - Assertions.assertThat(requesterSecret.get().getKey()).isEqualTo(requesterSecretKey); + Assertions.assertThat(requesterSecret.get().getHeader().getOnChainObjectAddress()).isEqualToIgnoringCase(""); + Assertions.assertThat(requesterSecret.get().getHeader().getKey()).isEqualTo(requesterSecretKey); Assertions.assertThat(requesterSecret.get().getValue()).isNotEqualTo(secretValue); Assertions.assertThat(requesterSecret.get().getValue()).isEqualTo(encryptionService.encrypt(secretValue)); @@ -204,7 +210,12 @@ void addMultipleRequesterSecrets() { } Assertions.assertThat(repository.count()).isEqualTo(keys.size()); List secrets = repository.findAll(); - Assertions.assertThat(secrets.stream().map(TeeTaskComputeSecret::getKey).collect(Collectors.toList())) + final List retrievedKeys = secrets + .stream() + .map(TeeTaskComputeSecret::getHeader) + .map(TeeTaskComputeSecretHeader::getKey) + .collect(Collectors.toList()); + Assertions.assertThat(retrievedKeys) .containsExactlyInAnyOrder("secret-key-1", "secret-key-2", "secret-key-3"); } diff --git a/src/itest/java/com/iexec/sms/tee/GramineBeansLoadingTests.java b/src/itest/java/com/iexec/sms/tee/GramineBeansLoadingTests.java new file mode 100644 index 00000000..b85d424f --- /dev/null +++ b/src/itest/java/com/iexec/sms/tee/GramineBeansLoadingTests.java @@ -0,0 +1,37 @@ +package com.iexec.sms.tee; + +import com.iexec.sms.api.config.GramineServicesProperties; +import com.iexec.sms.tee.session.gramine.GramineSessionHandlerService; +import com.iexec.sms.tee.session.gramine.GramineSessionMakerService; +import com.iexec.sms.tee.session.gramine.sps.SpsConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + + +@ActiveProfiles(profiles = {"gramine", "test"}) +public class GramineBeansLoadingTests extends TeeBeansLoadingTests { + @Autowired + GramineServicesProperties gramineServicesProperties; + @Autowired + GramineSessionHandlerService gramineSessionHandlerService; + @Autowired + GramineSessionMakerService gramineSessionMakerService; + @Autowired + SpsConfiguration spsConfiguration; + + GramineBeansLoadingTests(@Autowired Environment environment) { + super(environment); + } + + @Test + @Override + void checkTeeBeansAreLoaded() { + Assertions.assertNotNull(gramineServicesProperties); + Assertions.assertNotNull(gramineSessionHandlerService); + Assertions.assertNotNull(gramineSessionMakerService); + Assertions.assertNotNull(spsConfiguration); + } +} diff --git a/src/itest/java/com/iexec/sms/tee/SconeBeansLoadingTests.java b/src/itest/java/com/iexec/sms/tee/SconeBeansLoadingTests.java new file mode 100644 index 00000000..c5de91e3 --- /dev/null +++ b/src/itest/java/com/iexec/sms/tee/SconeBeansLoadingTests.java @@ -0,0 +1,45 @@ +package com.iexec.sms.tee; + +import com.iexec.sms.api.config.SconeServicesProperties; +import com.iexec.sms.tee.session.scone.SconeSessionHandlerService; +import com.iexec.sms.tee.session.scone.SconeSessionMakerService; +import com.iexec.sms.tee.session.scone.SconeSessionSecurityConfig; +import com.iexec.sms.tee.session.scone.cas.CasClient; +import com.iexec.sms.tee.session.scone.cas.CasConfiguration; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@ActiveProfiles(profiles = {"scone", "test"}) +public class SconeBeansLoadingTests extends TeeBeansLoadingTests { + @Autowired + SconeServicesProperties sconeServicesProperties; + @Autowired + SconeSessionHandlerService sconeSessionHandlerService; + @Autowired + SconeSessionMakerService sconeSessionMakerService; + @Autowired + SconeSessionSecurityConfig sconeSessionSecurityConfig; + @Autowired + CasClient casClient; + @Autowired + CasConfiguration casConfiguration; + + SconeBeansLoadingTests(@Autowired Environment environment) { + super(environment); + } + + @Test + @Override + void checkTeeBeansAreLoaded() { + assertNotNull(sconeServicesProperties); + assertNotNull(sconeSessionHandlerService); + assertNotNull(sconeSessionMakerService); + assertNotNull(sconeSessionSecurityConfig); + assertNotNull(casClient); + assertNotNull(casConfiguration); + } +} diff --git a/src/itest/java/com/iexec/sms/tee/TeeBeansLoadingTests.java b/src/itest/java/com/iexec/sms/tee/TeeBeansLoadingTests.java new file mode 100644 index 00000000..5322bd53 --- /dev/null +++ b/src/itest/java/com/iexec/sms/tee/TeeBeansLoadingTests.java @@ -0,0 +1,66 @@ +package com.iexec.sms.tee; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.CommonTestSetup; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.data.util.AnnotatedTypeScanner; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Arrays; +import java.util.Set; + +import static com.iexec.sms.MockChainConfiguration.MOCK_CHAIN_PROFILE; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Subclass this class to test whether some TEE beans are correctly loaded. + */ +@Slf4j +@ActiveProfiles(MOCK_CHAIN_PROFILE) +public abstract class TeeBeansLoadingTests extends CommonTestSetup { + + final AnnotatedTypeScanner annotatedTypeScanner; + final String[] activeProfiles; + + TeeBeansLoadingTests(@Autowired Environment environment) { + this.annotatedTypeScanner = new AnnotatedTypeScanner(true, ConditionalOnTeeFramework.class); + this.annotatedTypeScanner.setEnvironment(environment); + this.activeProfiles = environment.getActiveProfiles(); + } + + /** + * This will check that, for given active profiles, + * we don't load beans that should be disabled. + */ + @Test + void checkNoUnwantedBeanIsLoaded() { + final Set> sconeClasses = annotatedTypeScanner.findTypes("com", "iexec", "sms"); + for (Class clazz : sconeClasses) { + log.info("{} is loaded", clazz); + final TeeFramework[] frameworks = clazz.getAnnotation(ConditionalOnTeeFramework.class).frameworks(); + assertTrue( + areProfilesAndFrameworksMatching(frameworks), + clazz.getName() + " should not have been loaded [profiles:" + Arrays.toString(activeProfiles) + "]" + ); + } + } + + private boolean areProfilesAndFrameworksMatching(TeeFramework[] frameworks) { + for (TeeFramework framework : frameworks) { + for (String activeProfile : activeProfiles) { + if (framework.name().equalsIgnoreCase(activeProfile)) { + return true; + } + } + } + return false; + } + + /** + * Implement this method to check all beans of given TEE framework are loaded (= not null). + */ + abstract void checkTeeBeansAreLoaded(); +} diff --git a/src/itest/resources/application-test.yml b/src/itest/resources/application-test.yml index 775bcdc0..29ab0dc1 100644 --- a/src/itest/resources/application-test.yml +++ b/src/itest/resources/application-test.yml @@ -1,21 +1,24 @@ -# Those `server` properties are needed so that a random port is chosen for the HTTP listener -server: - http: - enabled: false - ssl: - enabled: false - # Run database in-mem spring: datasource: url: jdbc:h2:mem:db -# `TeeWorkflowConfiguration` needs some dummy value to be instantiated -tee.workflow: - las-image: "none" - pre-compute: - image: "none" - fingerprint: "none" - post-compute: - image: "none" - fingerprint: "none" +encryption: + # Will get previous key or else create one on this path + # this file shouldn't be clearly readable outside the enclave (but encrypted content could be copied outside) + aesKeyPath: src/test/resources/iexec-sms-aes.key + +tee: + worker: + pre-compute: + image: some + fingerprint: some + heap-size-gb: 3 + entrypoint: some + post-compute: + image: some + fingerprint: some + heap-size-gb: 3 + entrypoint: some + scone: + las-image: some diff --git a/src/main/java/com/iexec/sms/App.java b/src/main/java/com/iexec/sms/App.java index 1ab99d63..b3ef1200 100644 --- a/src/main/java/com/iexec/sms/App.java +++ b/src/main/java/com/iexec/sms/App.java @@ -16,21 +16,18 @@ package com.iexec.sms; -import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; @SpringBootApplication -@EnableFeignClients -@Slf4j +@ConfigurationPropertiesScan public class App { public static final String DOMAIN = "IEXEC_SMS_DOMAIN"; // TODO: Add session salt after domain public static void main(String[] args) { SpringApplication.run(App.class, args); - log.info("DEBUG - env: " + System.getenv().toString());//TODO: remove this later } } diff --git a/src/main/java/com/iexec/sms/AppController.java b/src/main/java/com/iexec/sms/AppController.java index ba2b2583..a6735a53 100644 --- a/src/main/java/com/iexec/sms/AppController.java +++ b/src/main/java/com/iexec/sms/AppController.java @@ -26,6 +26,7 @@ @RestController public class AppController { + //TODO: Remove this endpoint, use actuator endpoints instead. Update client too. @GetMapping(value = "/up") public ResponseEntity isUp() { String message = String.format("Up! (%s)", new Date()); diff --git a/src/main/java/com/iexec/sms/authorization/AuthorizationService.java b/src/main/java/com/iexec/sms/authorization/AuthorizationService.java index 31399575..b3e96c17 100644 --- a/src/main/java/com/iexec/sms/authorization/AuthorizationService.java +++ b/src/main/java/com/iexec/sms/authorization/AuthorizationService.java @@ -112,25 +112,6 @@ public boolean isSignedByOwner(String message, String signature, String address) return !owner.isEmpty() && isSignedByHimself(message, signature, owner); } - public String getChallengeForGetWeb3Secret(String secretAddress) { - return HashUtils.concatenateAndHash( - DOMAIN, - secretAddress); - } - - /* - * Note - These are equals: - * BytesUtils.bytesToString(Hash.sha3(DOMAIN.getBytes()) - * Hash.sha3String(DOMAIN) - * */ - public String getChallengeForGetWeb2Secret(String ownerAddress, - String secretKey) { - return HashUtils.concatenateAndHash( - Hash.sha3String(DOMAIN), - ownerAddress, - Hash.sha3String(secretKey)); - } - public String getChallengeForSetWeb3Secret(String secretAddress, String secretValue) { return HashUtils.concatenateAndHash( diff --git a/src/main/java/com/iexec/sms/config/HttpWebServerConfiguration.java b/src/main/java/com/iexec/sms/config/HttpWebServerConfiguration.java deleted file mode 100644 index 24a680cf..00000000 --- a/src/main/java/com/iexec/sms/config/HttpWebServerConfiguration.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.config; - -import org.apache.catalina.connector.Connector; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; -import org.springframework.boot.web.servlet.server.ServletWebServerFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class HttpWebServerConfiguration { - - @Value("${server.http.enabled}") - private boolean isHttpEnabled; - - @Value("${server.http.port}") - private int httpPort; - - /* - * This method will allow http connections (without SSL) if set in configuration - * */ - @Bean - public ServletWebServerFactory servletContainer() { - if (isHttpEnabled) { - return getHttpWebServerFactory(); - } - return getStandardWebServerFactory(); - } - - private TomcatServletWebServerFactory getHttpWebServerFactory() { - TomcatServletWebServerFactory factory = getStandardWebServerFactory(); - Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); - connector.setPort(httpPort); - factory.addAdditionalTomcatConnectors(connector); - return factory; - } - - private TomcatServletWebServerFactory getStandardWebServerFactory() { - return new TomcatServletWebServerFactory(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/iexec/sms/encryption/EncryptionConfiguration.java b/src/main/java/com/iexec/sms/encryption/EncryptionConfiguration.java index 70031c17..40c21757 100644 --- a/src/main/java/com/iexec/sms/encryption/EncryptionConfiguration.java +++ b/src/main/java/com/iexec/sms/encryption/EncryptionConfiguration.java @@ -16,19 +16,13 @@ package com.iexec.sms.encryption; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; -@Component -@Getter -@NoArgsConstructor -@AllArgsConstructor +@Data +@ConstructorBinding +@ConfigurationProperties(prefix = "encryption") public class EncryptionConfiguration { - - @Value("${encryption.aesKeyPath}") - private String aesKeyPath; - + private final String aesKeyPath; } diff --git a/src/main/java/com/iexec/sms/encryption/EncryptionService.java b/src/main/java/com/iexec/sms/encryption/EncryptionService.java index 50b1382c..deef1559 100644 --- a/src/main/java/com/iexec/sms/encryption/EncryptionService.java +++ b/src/main/java/com/iexec/sms/encryption/EncryptionService.java @@ -33,7 +33,7 @@ public class EncryptionService { private final String DEFAULT_MESSAGE = "Hello message to test AES key integrity"; - private byte[] aesKey; + private final byte[] aesKey; public EncryptionService(EncryptionConfiguration configuration) { this.aesKey = getOrCreateAesKey(configuration.getAesKeyPath()); diff --git a/src/main/java/com/iexec/sms/secret/AbstractSecretService.java b/src/main/java/com/iexec/sms/secret/AbstractSecretService.java index 2a1271ee..30a51325 100644 --- a/src/main/java/com/iexec/sms/secret/AbstractSecretService.java +++ b/src/main/java/com/iexec/sms/secret/AbstractSecretService.java @@ -20,26 +20,9 @@ public abstract class AbstractSecretService { - private final EncryptionService encryptionService; + protected final EncryptionService encryptionService; - public AbstractSecretService(EncryptionService encryptionService) { + protected AbstractSecretService(EncryptionService encryptionService) { this.encryptionService = encryptionService; } - - public Secret encryptSecret(Secret secret) { - if (!secret.isEncryptedValue()) { - String encrypted = encryptionService.encrypt(secret.getValue()); - secret.setValue(encrypted, true); - } - return secret; - } - - public Secret decryptSecret(Secret secret) { - if (secret.isEncryptedValue()) { - String decrypted = encryptionService.decrypt(secret.getValue()); - secret.setValue(decrypted, false); - } - return secret; - } - -} \ No newline at end of file +} diff --git a/src/main/java/com/iexec/sms/secret/Secret.java b/src/main/java/com/iexec/sms/secret/Secret.java index 096f238d..d7cc55ad 100644 --- a/src/main/java/com/iexec/sms/secret/Secret.java +++ b/src/main/java/com/iexec/sms/secret/Secret.java @@ -16,70 +16,33 @@ package com.iexec.sms.secret; -import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.GenericGenerator; import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; +import javax.persistence.MappedSuperclass; import java.util.Objects; -@Data +@MappedSuperclass @Getter -@AllArgsConstructor -@Entity -@NoArgsConstructor -public class Secret { - - @Id - @GeneratedValue(generator = "system-uuid") - @GenericGenerator(name = "system-uuid", strategy = "uuid") - private String id; - - private String address; //0xdataset1, aws.amazon.com, beneficiary.key.iex.ec (Kb) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class Secret { @Column(columnDefinition = "LONGTEXT") private String value; - private boolean isEncryptedValue; - - /* Clear secrets at construction */ - public Secret(String address, String value) { - this.address = address; - this.setValue(value, false); - } - - public void setValue(String value, boolean isEncryptedValue) { - this.value = value; - this.isEncryptedValue = isEncryptedValue; - } /** - * Get the secret value without possible leading or trailing + * Create the secret without possible leading or trailing * newline characters. This should be used when putting * the secret in the palaemon session. We decided to handle * this specific case because it has a good probability to occur * (when reading the secret from a file and uploading it to the * SMS without any trimming) and it can break the workflow even * though everything is correctly setup. - * - * @return trimmed secret value */ - public String getTrimmedValue() { - Objects.requireNonNull(this.value, "Secret value must not be null"); - return this.value.trim(); - } - - @Override - public String toString() { - return "Secret{" + - "id='" + id + '\'' + - ", address='" + address + '\'' + - ", value='" + (isEncryptedValue ? value : "") + '\'' + - ", isEncryptedValue=" + isEncryptedValue + - '}'; + protected Secret(String value) { + Objects.requireNonNull(value, "Secret value must not be null"); + this.value = value.trim(); } } diff --git a/src/main/java/com/iexec/sms/secret/SecretController.java b/src/main/java/com/iexec/sms/secret/SecretController.java index f61af5b0..28347000 100644 --- a/src/main/java/com/iexec/sms/secret/SecretController.java +++ b/src/main/java/com/iexec/sms/secret/SecretController.java @@ -17,19 +17,18 @@ package com.iexec.sms.secret; -import com.iexec.common.security.Signature; import com.iexec.sms.authorization.AuthorizationService; -import com.iexec.sms.secret.web2.Web2SecretsService; -import com.iexec.sms.secret.web3.Web3Secret; +import com.iexec.sms.secret.web2.NotAnExistingSecretException; +import com.iexec.sms.secret.web2.SameSecretException; +import com.iexec.sms.secret.web2.SecretAlreadyExistsException; +import com.iexec.sms.secret.web2.Web2SecretService; import com.iexec.sms.secret.web3.Web3SecretService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.Optional; - -import static com.iexec.common.utils.SignatureUtils.signMessageHashAndGetSignature; +import java.util.NoSuchElementException; @Slf4j @CrossOrigin @@ -39,12 +38,12 @@ public class SecretController { private final AuthorizationService authorizationService; private final Web3SecretService web3SecretService; - private final Web2SecretsService web2SecretsService; + private final Web2SecretService web2SecretService; public SecretController(AuthorizationService authorizationService, - Web2SecretsService web2SecretsService, + Web2SecretService web2SecretService, Web3SecretService web3SecretService) { - this.web2SecretsService = web2SecretsService; + this.web2SecretService = web2SecretService; this.authorizationService = authorizationService; this.web3SecretService = web3SecretService; } @@ -52,29 +51,14 @@ public SecretController(AuthorizationService authorizationService, // Web3 @RequestMapping(path = "/web3", method = RequestMethod.HEAD) - public ResponseEntity isWeb3SecretSet(@RequestParam String secretAddress) { - Optional secret = web3SecretService.getSecret(secretAddress); - return secret.map(body -> ResponseEntity.noContent().build()).orElseGet(() -> ResponseEntity.notFound().build()); - } - - @GetMapping("/web3") - public ResponseEntity getWeb3Secret(@RequestHeader("Authorization") String authorization, - @RequestParam String secretAddress, - @RequestParam(required = false, defaultValue = "false") boolean shouldDecryptSecret) { - String challenge = authorizationService.getChallengeForGetWeb3Secret(secretAddress); - - //TODO: also isAuthorizedOnExecution(..) - if (!authorizationService.isSignedByOwner(challenge, authorization, secretAddress)) { - log.error("Unauthorized to getWeb3Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - Optional secret = web3SecretService.getSecret(secretAddress, shouldDecryptSecret); - return secret.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + public ResponseEntity isWeb3SecretSet(@RequestParam String secretAddress) { + return web3SecretService.isSecretPresent(secretAddress) + ? ResponseEntity.noContent().build() + : ResponseEntity.notFound().build(); } @PostMapping("/web3") - public ResponseEntity addWeb3Secret(@RequestHeader("Authorization") String authorization, + public ResponseEntity addWeb3Secret(@RequestHeader String authorization, @RequestParam String secretAddress, @RequestBody String secretValue) { if (!SecretUtils.isSecretSizeValid(secretValue)) { @@ -88,41 +72,25 @@ public ResponseEntity addWeb3Secret(@RequestHeader("Authorization") Stri return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - if (web3SecretService.getSecret(secretAddress).isPresent()) { + if (!web3SecretService.addSecret(secretAddress, secretValue)) { return ResponseEntity.status(HttpStatus.CONFLICT).build(); // secret already exists } - web3SecretService.addSecret(secretAddress, secretValue); return ResponseEntity.noContent().build(); } // Web2 @RequestMapping(path = "/web2", method = RequestMethod.HEAD) - public ResponseEntity isWeb2SecretSet(@RequestParam String ownerAddress, - @RequestParam String secretName) { - Optional secret = web2SecretsService.getSecret(ownerAddress, secretName, false); - return secret.map(body -> ResponseEntity.noContent().build()).orElseGet(() -> ResponseEntity.notFound().build()); - } - - @GetMapping("/web2") - public ResponseEntity getWeb2Secret(@RequestHeader("Authorization") String authorization, - @RequestParam String ownerAddress, - @RequestParam String secretName, - @RequestParam(required = false, defaultValue = "false") boolean shouldDecryptSecret) { - String challenge = authorizationService.getChallengeForGetWeb2Secret(ownerAddress, secretName); - - if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { - log.error("Unauthorized to getWeb2Secret [expectedChallenge:{}]", challenge); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - Optional secret = web2SecretsService.getSecret(ownerAddress, secretName, shouldDecryptSecret); - return secret.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); + public ResponseEntity isWeb2SecretSet(@RequestParam String ownerAddress, + @RequestParam String secretName) { + return web2SecretService.isSecretPresent(ownerAddress, secretName) + ? ResponseEntity.noContent().build() + : ResponseEntity.notFound().build(); } @PostMapping("/web2") - public ResponseEntity addWeb2Secret(@RequestHeader("Authorization") String authorization, + public ResponseEntity addWeb2Secret(@RequestHeader String authorization, @RequestParam String ownerAddress, @RequestParam String secretName, @RequestBody String secretValue) { @@ -137,19 +105,23 @@ public ResponseEntity addWeb2Secret(@RequestHeader("Authorization") Stri return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - if (web2SecretsService.getSecret(ownerAddress, secretName).isPresent()) { + try { + web2SecretService.addSecret(ownerAddress, secretName, secretValue); + return ResponseEntity.noContent().build(); + } catch (SecretAlreadyExistsException e) { return ResponseEntity.status(HttpStatus.CONFLICT).build(); } - - web2SecretsService.addSecret(ownerAddress, secretName, secretValue); - return ResponseEntity.noContent().build(); } @PutMapping("/web2") - public ResponseEntity updateWeb2Secret(@RequestHeader("Authorization") String authorization, + public ResponseEntity updateWeb2Secret(@RequestHeader String authorization, @RequestParam String ownerAddress, @RequestParam String secretName, @RequestBody String newSecretValue) { + if (!SecretUtils.isSecretSizeValid(newSecretValue)) { + return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build(); + } + String challenge = authorizationService.getChallengeForSetWeb2Secret(ownerAddress, secretName, newSecretValue); if (!authorizationService.isSignedByHimself(challenge, authorization, ownerAddress)) { @@ -157,27 +129,15 @@ public ResponseEntity updateWeb2Secret(@RequestHeader("Authorization") S return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } - if (web2SecretsService.getSecret(ownerAddress, secretName).isEmpty()) { + try { + web2SecretService.updateSecret(ownerAddress, secretName, newSecretValue); + return ResponseEntity.noContent().build(); + } catch (SameSecretException ignored) { + return ResponseEntity.noContent().build(); + } catch (NoSuchElementException | NotAnExistingSecretException e) { return ResponseEntity.notFound().build(); } - - web2SecretsService.updateSecret(ownerAddress, secretName, newSecretValue); - return ResponseEntity.noContent().build(); } - /* - * Server-side signature of a messageHash - * */ - @PostMapping("/delegate/signature") - public ResponseEntity signMessageHashOnServerSide(@RequestParam String messageHash, - @RequestBody String privateKey) { - Signature signature = signMessageHashAndGetSignature(messageHash, privateKey); - - if (signature.getValue() == null || signature.getValue().isEmpty()) { - return ResponseEntity.notFound().build(); - } - - return ResponseEntity.ok(signature.getValue()); - } } diff --git a/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java b/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java index f52a3810..7bd4cfbd 100644 --- a/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java +++ b/src/main/java/com/iexec/sms/secret/compute/AppComputeSecretController.java @@ -42,8 +42,8 @@ public class AppComputeSecretController { static final String INVALID_SECRET_KEY_FORMAT_MSG = "Secret key should contain at most 64 characters from [0-9A-Za-z-_]"; private static final Pattern secretKeyPattern = Pattern.compile("^[\\p{Alnum}-_]{" - + TeeTaskComputeSecret.SECRET_KEY_MIN_LENGTH + "," - + TeeTaskComputeSecret.SECRET_KEY_MAX_LENGTH + "}$"); + + TeeTaskComputeSecretHeader.SECRET_KEY_MIN_LENGTH + "," + + TeeTaskComputeSecretHeader.SECRET_KEY_MAX_LENGTH + "}$"); public AppComputeSecretController(AuthorizationService authorizationService, TeeTaskComputeSecretService teeTaskComputeSecretService) { diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java index 7658abb1..77b62f91 100644 --- a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecret.java @@ -17,15 +17,14 @@ package com.iexec.sms.secret.compute; import com.iexec.sms.secret.SecretUtils; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.GenericGenerator; +import lombok.*; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import java.io.Serializable; +import java.util.Objects; /** * Define a secret that can be used during the execution of a TEE task. @@ -47,37 +46,16 @@ *
  • For requesters, it must be a String of at most 64 characters from [0-9A-Za-z-_]. * */ -@Data -@NoArgsConstructor @Entity -@Table(uniqueConstraints = { @UniqueConstraint(columnNames = {"onChainObjectAddress", "fixedSecretOwner", "key"}) }) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class TeeTaskComputeSecret implements Serializable { - public static final int SECRET_KEY_MIN_LENGTH = 1; - public static final int SECRET_KEY_MAX_LENGTH = 64; - - @Id - @GeneratedValue(generator = "system-uuid") - @GenericGenerator(name = "system-uuid", strategy = "uuid") - private String id; - - /** - * Represents the blockchain address of the deployed object - * (0xapplication, 0xdataset, 0xworkerpool) - *

    - * In a future release, it should also handle ENS names. - */ - @NotNull - private String onChainObjectAddress; // Will be empty for a secret belonging to a requester - @NotNull - private OnChainObjectType onChainObjectType; - @NotNull - private SecretOwnerRole secretOwnerRole; - @NotNull - private String fixedSecretOwner; // Will be empty for a secret belonging to an application developer @NotNull - @Size(min = SECRET_KEY_MIN_LENGTH, max = SECRET_KEY_MAX_LENGTH) - private String key; + @EmbeddedId + private TeeTaskComputeSecretHeader header; + @NotNull /* * Expected behavior of AES encryption is to not expand the data very much. @@ -102,11 +80,31 @@ public TeeTaskComputeSecret( String fixedSecretOwner, String key, String value) { - this.onChainObjectType = onChainObjectType; - this.onChainObjectAddress = onChainObjectAddress; - this.secretOwnerRole = secretOwnerRole; - this.fixedSecretOwner = fixedSecretOwner; - this.key = key; + this.header = new TeeTaskComputeSecretHeader( + onChainObjectType, + onChainObjectAddress, + secretOwnerRole, + fixedSecretOwner, + key + ); this.value = value; } + + public TeeTaskComputeSecret withValue(String newValue) { + return new TeeTaskComputeSecret(header, newValue); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TeeTaskComputeSecret that = (TeeTaskComputeSecret) o; + return Objects.equals(header, that.header) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(header, value); + } } diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeader.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeader.java new file mode 100644 index 00000000..3457eb26 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeader.java @@ -0,0 +1,110 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret.compute; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.persistence.Embeddable; +import javax.validation.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Embeddable +@Slf4j +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TeeTaskComputeSecretHeader implements Serializable { + public static final int SECRET_KEY_MIN_LENGTH = 1; + public static final int SECRET_KEY_MAX_LENGTH = 64; + + @NotNull + private OnChainObjectType onChainObjectType; + /** + * Represents the blockchain address of the deployed object + * (0xapplication, 0xdataset, 0xworkerpool) + * as a lower case string. + *

    + * In a future release, it should also handle ENS names. + */ + @NotNull + private String onChainObjectAddress; // Will be empty for a secret belonging to a requester + @NotNull + private SecretOwnerRole secretOwnerRole; + @NotNull + private String fixedSecretOwner; // Will be empty for a secret belonging to an application developer + @NotNull + @Size(min = SECRET_KEY_MIN_LENGTH, max = SECRET_KEY_MAX_LENGTH) + private String key; + + public TeeTaskComputeSecretHeader(OnChainObjectType onChainObjectType, + String onChainObjectAddress, + SecretOwnerRole secretOwnerRole, + String fixedSecretOwner, + String key) { + if (secretOwnerRole == SecretOwnerRole.REQUESTER && !StringUtils.isEmpty(onChainObjectAddress)) { + throw new ValidationException("On-chain object address should be empty for a requester secret."); + } + + if (secretOwnerRole == SecretOwnerRole.APPLICATION_DEVELOPER && !StringUtils.isEmpty(fixedSecretOwner)) { + throw new ValidationException("Fixed secret owner should be empty for an application developer secret."); + } + + this.onChainObjectAddress = onChainObjectAddress == null ? "" : onChainObjectAddress.toLowerCase(); + this.onChainObjectType = onChainObjectType; + this.secretOwnerRole = secretOwnerRole; + this.fixedSecretOwner = fixedSecretOwner == null ? "" : fixedSecretOwner.toLowerCase(); + this.key = key; + + validateFields(); + } + + private void validateFields() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + final Validator validator = factory.getValidator(); + final Set> issues = validator.validate(this); + if (!issues.isEmpty()) { + log.warn("{}", issues.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList())); + throw new ValidationException("Can't create TeeTaskComputeSecretHeader."); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TeeTaskComputeSecretHeader that = (TeeTaskComputeSecretHeader) o; + return onChainObjectType == that.onChainObjectType + && Objects.equals(onChainObjectAddress, that.onChainObjectAddress) + && secretOwnerRole == that.secretOwnerRole + && Objects.equals(fixedSecretOwner, that.fixedSecretOwner) + && Objects.equals(key, that.key); + } + + @Override + public int hashCode() { + return Objects.hash(onChainObjectType, onChainObjectAddress, secretOwnerRole, fixedSecretOwner, key); + } +} diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java index 88180feb..2803b6b3 100644 --- a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretRepository.java @@ -18,5 +18,5 @@ import org.springframework.data.jpa.repository.JpaRepository; -public interface TeeTaskComputeSecretRepository extends JpaRepository { +public interface TeeTaskComputeSecretRepository extends JpaRepository { } diff --git a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java index 0c321b04..3969b44d 100644 --- a/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java +++ b/src/main/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretService.java @@ -18,9 +18,6 @@ import com.iexec.sms.encryption.EncryptionService; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.SerializationUtils; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; import org.springframework.stereotype.Service; import java.util.Optional; @@ -48,28 +45,21 @@ public Optional getSecret( SecretOwnerRole secretOwnerRole, String secretOwner, String secretKey) { - onChainObjectAddress = onChainObjectAddress.toLowerCase(); - final TeeTaskComputeSecret wantedSecret = TeeTaskComputeSecret - .builder() - .onChainObjectType(onChainObjectType) - .onChainObjectAddress(onChainObjectAddress) - .secretOwnerRole(secretOwnerRole) - .fixedSecretOwner(secretOwner) - .key(secretKey) - .build(); - final ExampleMatcher exampleMatcher = ExampleMatcher.matching() - .withIgnorePaths("value"); + final TeeTaskComputeSecretHeader header = new TeeTaskComputeSecretHeader( + onChainObjectType, + onChainObjectAddress, + secretOwnerRole, + secretOwner, + secretKey + ); final Optional oSecret = teeTaskComputeSecretRepository - .findOne(Example.of(wantedSecret, exampleMatcher)); + .findById(header); if (oSecret.isEmpty()) { return Optional.empty(); } final TeeTaskComputeSecret secret = oSecret.get(); final String decryptedValue = encryptionService.decrypt(secret.getValue()); - // deep copy to avoid altering original object - //TODO: Improve this out-of-the box cloning to get better performances - TeeTaskComputeSecret decryptedSecret = SerializationUtils.clone(secret); - decryptedSecret.setValue(decryptedValue); + TeeTaskComputeSecret decryptedSecret = secret.withValue(decryptedValue); return Optional.of(decryptedSecret); } @@ -116,7 +106,6 @@ public boolean encryptAndSaveSecret(OnChainObjectType onChainObjectType, " [secret:{}]", secret); return false; } - onChainObjectAddress = onChainObjectAddress.toLowerCase(); final TeeTaskComputeSecret secret = TeeTaskComputeSecret .builder() .onChainObjectType(onChainObjectType) diff --git a/src/main/java/com/iexec/sms/secret/web2/NotAnExistingSecretException.java b/src/main/java/com/iexec/sms/secret/web2/NotAnExistingSecretException.java new file mode 100644 index 00000000..a85749f4 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/NotAnExistingSecretException.java @@ -0,0 +1,14 @@ +package com.iexec.sms.secret.web2; + +import lombok.Getter; + +@Getter +public class NotAnExistingSecretException extends Exception { + private final String ownerAddress; + private final String secretAddress; + + public NotAnExistingSecretException(String ownerAddress, String secretAddress) { + this.ownerAddress = ownerAddress; + this.secretAddress = secretAddress; + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/SameSecretException.java b/src/main/java/com/iexec/sms/secret/web2/SameSecretException.java new file mode 100644 index 00000000..af06678b --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/SameSecretException.java @@ -0,0 +1,14 @@ +package com.iexec.sms.secret.web2; + +import lombok.Getter; + +@Getter +public class SameSecretException extends Exception { + private final String ownerAddress; + private final String secretAddress; + + public SameSecretException(String ownerAddress, String secretAddress) { + this.ownerAddress = ownerAddress; + this.secretAddress = secretAddress; + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/SecretAlreadyExistsException.java b/src/main/java/com/iexec/sms/secret/web2/SecretAlreadyExistsException.java new file mode 100644 index 00000000..eac3d0bb --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/SecretAlreadyExistsException.java @@ -0,0 +1,14 @@ +package com.iexec.sms.secret.web2; + +import lombok.Getter; + +@Getter +public class SecretAlreadyExistsException extends Exception { + private final String ownerAddress; + private final String secretAddress; + + public SecretAlreadyExistsException(String ownerAddress, String secretAddress) { + this.ownerAddress = ownerAddress; + this.secretAddress = secretAddress; + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2Secret.java b/src/main/java/com/iexec/sms/secret/web2/Web2Secret.java new file mode 100644 index 00000000..95030797 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/Web2Secret.java @@ -0,0 +1,55 @@ +/* + * + * * Copyright 2022 IEXEC BLOCKCHAIN TECH + * * + * * 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. + * + */ + +package com.iexec.sms.secret.web2; + +import com.iexec.sms.secret.Secret; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Web2Secret extends Secret { + @EmbeddedId + private Web2SecretHeader header; + + public Web2Secret(String ownerAddress, String address, String value) { + this(new Web2SecretHeader(ownerAddress, address), value); + } + + private Web2Secret(Web2SecretHeader header, String value) { + super(value); + this.header = header; + } + + /** + * Copies the current {@link Web2Secret} object, + * while replacing the old value with a new value. + * + * @param newValue Value to use for new object. + * @return A new {@link Web2Secret} object with new value. + */ + public Web2Secret withValue(String newValue) { + return new Web2Secret(header, newValue); + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretHeader.java b/src/main/java/com/iexec/sms/secret/web2/Web2SecretHeader.java new file mode 100644 index 00000000..4afebe95 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/Web2SecretHeader.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret.web2; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Web2SecretHeader implements Serializable { + private static final long serialVersionUID = -6126999592529129002L; + private String ownerAddress; + private String address; //0xdataset1, aws.amazon.com, beneficiary.key.iex.ec (Kb) + + Web2SecretHeader(String ownerAddress, String address) { + Objects.requireNonNull(ownerAddress, "Web2 secret owner address can't be null."); + Objects.requireNonNull(address, "Web2 secret address can't be null."); + + this.ownerAddress = ownerAddress.toLowerCase(); + this.address = address.toLowerCase(); + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretRepository.java b/src/main/java/com/iexec/sms/secret/web2/Web2SecretRepository.java new file mode 100644 index 00000000..cbeacde1 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/Web2SecretRepository.java @@ -0,0 +1,25 @@ +/* + * + * * Copyright 2022 IEXEC BLOCKCHAIN TECH + * * + * * 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. + * + */ + +package com.iexec.sms.secret.web2; + +import org.springframework.data.repository.CrudRepository; + +public interface Web2SecretRepository extends CrudRepository { + +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretService.java b/src/main/java/com/iexec/sms/secret/web2/Web2SecretService.java new file mode 100644 index 00000000..112f5268 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web2/Web2SecretService.java @@ -0,0 +1,115 @@ +/* + * + * * Copyright 2022 IEXEC BLOCKCHAIN TECH + * * + * * 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. + * + */ + +package com.iexec.sms.secret.web2; + +import com.iexec.sms.encryption.EncryptionService; +import com.iexec.sms.secret.AbstractSecretService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Objects; +import java.util.Optional; + +@Slf4j +@Service +public class Web2SecretService extends AbstractSecretService { + private final Web2SecretRepository web2SecretRepository; + + protected Web2SecretService(EncryptionService encryptionService, + Web2SecretRepository web2SecretRepository) { + super(encryptionService); + this.web2SecretRepository = web2SecretRepository; + } + + /** + * Get the secret as it was saved in DB. + * Its value should then be encrypted. + * + * @param ownerAddress Address of the secret owner. + * @param secretAddress Address of the secret. + * @return An empty {@link Optional} if no secret is found, + * an {@link Optional} containing the secret if it exists. + */ + Optional getSecret(String ownerAddress, String secretAddress) { + return web2SecretRepository.findById(new Web2SecretHeader(ownerAddress, secretAddress)); + } + + public Optional getDecryptedValue(String ownerAddress, String secretAddress) { + return getSecret(ownerAddress, secretAddress) + .map(secret -> encryptionService.decrypt(secret.getValue())); + } + + public boolean isSecretPresent(String ownerAddress, String secretAddress) { + return getSecret(ownerAddress, secretAddress).isPresent(); + } + + /** + * Creates and saves a new {@link Web2Secret}. + * If a secret with same {@code ownerAddress}/{@code secretAddress} couple already exists, then cancels the save. + * + * @param ownerAddress Address of the secret owner. + * @param secretAddress Address of the secret. + * @param secretValue Unencrypted value of the secret. + * @return The {@link Web2Secret} that has been saved. + * @throws SecretAlreadyExistsException throw when a secret + * with same {@code ownerAddress}/{@code secretAddress} couple already exists + */ + public Web2Secret addSecret(String ownerAddress, String secretAddress, String secretValue) throws SecretAlreadyExistsException { + if (isSecretPresent(ownerAddress, secretAddress)) { + log.error("Secret already exists [ownerAddress:{}, secretAddress:{}]", ownerAddress, secretAddress); + throw new SecretAlreadyExistsException(ownerAddress, secretAddress); + } + + final String encryptedValue = encryptionService.encrypt(secretValue); + final Web2Secret newSecret = new Web2Secret(ownerAddress, secretAddress, encryptedValue); + return web2SecretRepository.save(newSecret); + } + + /** + * Updates an existing {@link Web2Secret}. + * If the secret does not already exist, then cancels the save. + * If the secret already exists with the same encrypted value, then cancels the save. + * + * @param ownerAddress Address of the secret owner. + * @param secretAddress Address of the secret. + * @param newSecretValue New, unencrypted value of the secret. + * @return The {@link Web2Secret} that has been saved. + * @throws NotAnExistingSecretException thrown when the requested secret does not exist. + * @throws SameSecretException thrown when the requested secret already contains the encrypted value. + */ + public Web2Secret updateSecret(String ownerAddress, String secretAddress, String newSecretValue) throws NotAnExistingSecretException, SameSecretException { + final Optional oSecret = getSecret(ownerAddress, secretAddress); + if (oSecret.isEmpty()) { + log.error("Secret does not exist, can't update it [ownerAddress:{}, secretAddress:{}]", + ownerAddress, secretAddress); + throw new NotAnExistingSecretException(ownerAddress, secretAddress); + } + + final Web2Secret secret = oSecret.get(); + final String encryptedValue = encryptionService.encrypt(newSecretValue); + if (Objects.equals(secret.getValue(), encryptedValue)) { + log.info("No need to update secret [ownerAddress:{}, secretAddress:{}]", + ownerAddress, secretAddress); + throw new SameSecretException(ownerAddress, secretAddress); + } + + final Web2Secret newSecret = secret.withValue(encryptedValue); + return web2SecretRepository.save(newSecret); + } +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2Secrets.java b/src/main/java/com/iexec/sms/secret/web2/Web2Secrets.java deleted file mode 100644 index dccdebd9..00000000 --- a/src/main/java/com/iexec/sms/secret/web2/Web2Secrets.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.secret.web2; - -import com.iexec.sms.secret.Secret; -import lombok.Data; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.GenericGenerator; - -import javax.persistence.*; -import java.util.ArrayList; -import java.util.List; - -@Data -@Getter -@Entity -@NoArgsConstructor -public class Web2Secrets { - - @Id - @GeneratedValue(generator = "system-uuid") - @GenericGenerator(name = "system-uuid", strategy = "uuid") - private String id; - - private String ownerAddress; - @OneToMany(cascade = {CascadeType.ALL}) - private List secrets; - - Web2Secrets(String ownerAddress) { - this.ownerAddress = ownerAddress; - this.secrets = new ArrayList<>(); - } - - public Secret getSecret(String secretAddress) { - for (Secret secret : secrets) { - if (secret.getAddress().equals(secretAddress)) { - return secret; - } - } - return null; - } -} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java b/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java deleted file mode 100644 index 4122451f..00000000 --- a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsService.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.secret.web2; - - -import com.iexec.sms.encryption.EncryptionService; -import com.iexec.sms.secret.AbstractSecretService; -import com.iexec.sms.secret.Secret; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Slf4j -@Service -public class Web2SecretsService extends AbstractSecretService { - - private final Web2SecretsRepository web2SecretsRepository; - - public Web2SecretsService(Web2SecretsRepository web2SecretsRepository, - EncryptionService encryptionService) { - super(encryptionService); - this.web2SecretsRepository = web2SecretsRepository; - } - - public Optional getWeb2Secrets(String ownerAddress) { - ownerAddress = ownerAddress.toLowerCase(); - return web2SecretsRepository.findWeb2SecretsByOwnerAddress(ownerAddress); - } - - public Optional getSecret(String ownerAddress, String secretAddress) { - return getSecret(ownerAddress, secretAddress, false); - } - - public Optional getSecret(String ownerAddress, String secretAddress, boolean shouldDecryptValue) { - ownerAddress = ownerAddress.toLowerCase(); - Optional web2Secrets = getWeb2Secrets(ownerAddress); - if (web2Secrets.isEmpty()) { - return Optional.empty(); - } - Secret secret = web2Secrets.get().getSecret(secretAddress); - if (secret == null) { - return Optional.empty(); - } - if (shouldDecryptValue) { - decryptSecret(secret); - } - return Optional.of(secret); - } - - public void addSecret(String ownerAddress, String secretAddress, String secretValue) { - ownerAddress = ownerAddress.toLowerCase(); - Web2Secrets web2Secrets = new Web2Secrets(ownerAddress); - Optional existingWeb2Secrets = getWeb2Secrets(ownerAddress); - if (existingWeb2Secrets.isPresent()) { - web2Secrets = existingWeb2Secrets.get(); - } - - Secret secret = new Secret(secretAddress, secretValue); - encryptSecret(secret); - log.info("Adding new secret [ownerAddress:{}, secretAddress:{}, encryptedSecretValue:{}]", - ownerAddress, secretAddress, secret.getValue()); - web2Secrets.getSecrets().add(secret); - web2SecretsRepository.save(web2Secrets); - } - - public void updateSecret(String ownerAddress, String secretAddress, String newSecretValue) { - ownerAddress = ownerAddress.toLowerCase(); - Secret newSecret = new Secret(secretAddress, newSecretValue); - encryptSecret(newSecret); - Optional web2Secrets = getWeb2Secrets(ownerAddress); - Secret existingSecret = web2Secrets.get().getSecret(secretAddress); - if (existingSecret.getValue().equals(newSecret.getValue())) { - log.info("No need to update secret [ownerAddress:{}, secretAddress:{}]", - ownerAddress, secretAddress); - return; - } - - log.info("Updating secret [ownerAddress:{}, secretAddress:{}, oldEncryptedSecretValue:{}, newEncryptedSecretValue:{}]", - ownerAddress, secretAddress, existingSecret.getValue(), newSecret.getValue()); - existingSecret.setValue(newSecret.getValue(), true); - web2SecretsRepository.save(web2Secrets.get()); - } -} diff --git a/src/main/java/com/iexec/sms/secret/web3/Web3Secret.java b/src/main/java/com/iexec/sms/secret/web3/Web3Secret.java index 570f2a8d..75f35292 100644 --- a/src/main/java/com/iexec/sms/secret/web3/Web3Secret.java +++ b/src/main/java/com/iexec/sms/secret/web3/Web3Secret.java @@ -17,29 +17,26 @@ package com.iexec.sms.secret.web3; import com.iexec.sms.secret.Secret; -import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.GenericGenerator; +import javax.persistence.EmbeddedId; import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.Id; -@EqualsAndHashCode(callSuper = true) -@Data -@Getter @Entity -@NoArgsConstructor +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Web3Secret extends Secret { - - @Id - @GeneratedValue(generator = "system-uuid") - @GenericGenerator(name = "system-uuid", strategy = "uuid") - private String id; + @EmbeddedId + private Web3SecretHeader header; public Web3Secret(String address, String value) { - super(address, value); + this(new Web3SecretHeader(address), value); + } + + private Web3Secret(Web3SecretHeader header, String value) { + super(value); + this.header = header; } } diff --git a/src/main/java/com/iexec/sms/secret/web3/Web3SecretHeader.java b/src/main/java/com/iexec/sms/secret/web3/Web3SecretHeader.java new file mode 100644 index 00000000..be856cb2 --- /dev/null +++ b/src/main/java/com/iexec/sms/secret/web3/Web3SecretHeader.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret.web3; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Web3SecretHeader implements Serializable { + private static final long serialVersionUID = -6181164795694317827L; + private String address; + + Web3SecretHeader(String address) { + Objects.requireNonNull(address, "Web3 secret address can't be null."); + + this.address = address.toLowerCase(); + } +} diff --git a/src/main/java/com/iexec/sms/secret/web3/Web3SecretRepository.java b/src/main/java/com/iexec/sms/secret/web3/Web3SecretRepository.java index 9b2d39c5..6a23e496 100644 --- a/src/main/java/com/iexec/sms/secret/web3/Web3SecretRepository.java +++ b/src/main/java/com/iexec/sms/secret/web3/Web3SecretRepository.java @@ -16,13 +16,8 @@ package com.iexec.sms.secret.web3; - import org.springframework.data.repository.CrudRepository; -import java.util.Optional; - -public interface Web3SecretRepository extends CrudRepository { - - Optional findWeb3SecretByAddress(String secretAddress); +public interface Web3SecretRepository extends CrudRepository { } diff --git a/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java b/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java index ecf197b8..fbd3f5c1 100644 --- a/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java +++ b/src/main/java/com/iexec/sms/secret/web3/Web3SecretService.java @@ -36,33 +36,44 @@ public Web3SecretService(Web3SecretRepository web3SecretRepository, this.web3SecretRepository = web3SecretRepository; } - public Optional getSecret(String secretAddress, boolean shouldDecryptValue) { - secretAddress = secretAddress.toLowerCase(); - Optional secret = web3SecretRepository.findWeb3SecretByAddress(secretAddress); - if (secret.isEmpty()) { - return Optional.empty(); - } - if (shouldDecryptValue) { - decryptSecret(secret.get()); - } - return secret; + /** + * Get the secret as it was saved in DB. + * Its value should then be encrypted. + * + * @param secretAddress Address of the secret. + * @return An empty {@link Optional} if no secret is found, + * an {@link Optional} containing the secret if it exists. + */ + Optional getSecret(String secretAddress) { + return web3SecretRepository.findById(new Web3SecretHeader(secretAddress)); + } + + public Optional getDecryptedValue(String secretAddress) { + return getSecret(secretAddress) + .map(secret -> encryptionService.decrypt(secret.getValue())); } - public Optional getSecret(String secretAddress) { - return getSecret(secretAddress, false); + public boolean isSecretPresent(String secretAddress) { + return getSecret(secretAddress).isPresent(); } /* * * Stores encrypted secrets * */ - public void addSecret(String secretAddress, String secretValue) { - secretAddress = secretAddress.toLowerCase(); - Web3Secret web3Secret = new Web3Secret(secretAddress, secretValue); - encryptSecret(web3Secret); + public boolean addSecret(String secretAddress, String secretValue) { + if (isSecretPresent(secretAddress)) { + log.error("Secret already exists [secretAddress:{}]", secretAddress); + return false; + } + + final String encryptedValue = encryptionService.encrypt(secretValue); log.info("Adding new web3 secret [secretAddress:{}, encryptedSecretValue:{}]", - secretAddress, web3Secret.getValue()); + secretAddress, encryptedValue); + + final Web3Secret web3Secret = new Web3Secret(secretAddress, encryptedValue); web3SecretRepository.save(web3Secret); + return true; } } diff --git a/src/main/java/com/iexec/sms/ssl/SslConfig.java b/src/main/java/com/iexec/sms/ssl/SslConfig.java index 78eb0c54..9f8a0ffd 100644 --- a/src/main/java/com/iexec/sms/ssl/SslConfig.java +++ b/src/main/java/com/iexec/sms/ssl/SslConfig.java @@ -16,6 +16,9 @@ package com.iexec.sms.ssl; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import lombok.extern.slf4j.Slf4j; import org.apache.http.ssl.SSLContexts; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -29,19 +32,21 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; +@Slf4j @Configuration +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) public class SslConfig { - private String sslKeystore; - private String sslKeystoreType; - private String sslKeyAlias; - private char[] sslKeystorePasswordChar; + private final String sslKeystore; + private final String sslKeystoreType; + private final String sslKeyAlias; + private final char[] sslKeystorePasswordChar; public SslConfig( - @Value("${server.ssl.key-store}") String sslKeystore, - @Value("${server.ssl.key-store-type}") String sslKeystoreType, - @Value("${server.ssl.key-alias}") String sslKeyAlias, - @Value("${server.ssl.key-store-password}") String sslKeystorePassword) { + @Value("${tee.ssl.key-store}") String sslKeystore, + @Value("${tee.ssl.key-store-type}") String sslKeystoreType, + @Value("${tee.ssl.key-alias}") String sslKeyAlias, + @Value("${tee.ssl.key-store-password}") String sslKeystorePassword) { this.sslKeystore = sslKeystore; this.sslKeystoreType = sslKeystoreType; this.sslKeyAlias = sslKeyAlias; @@ -62,7 +67,7 @@ public SSLContext getFreshSslContext() { .loadTrustMaterial(null, (chain, authType) -> true)////TODO: Add CAS certificate to truststore .build(); } catch (IOException | NoSuchAlgorithmException | KeyStoreException | UnrecoverableKeyException | CertificateException | KeyManagementException e) { - e.printStackTrace(); + log.warn("Failed to create a fresh SSL context", e); } return null; } diff --git a/src/main/java/com/iexec/sms/ssl/TwoWaySslClient.java b/src/main/java/com/iexec/sms/ssl/TwoWaySslClient.java index 9056488c..c6b0aad5 100644 --- a/src/main/java/com/iexec/sms/ssl/TwoWaySslClient.java +++ b/src/main/java/com/iexec/sms/ssl/TwoWaySslClient.java @@ -16,6 +16,8 @@ package com.iexec.sms.ssl; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.ConditionalOnTeeFramework; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.context.annotation.Configuration; @@ -23,6 +25,7 @@ import org.springframework.web.client.RestTemplate; @Configuration +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) public class TwoWaySslClient { private final SslConfig sslConfig; diff --git a/src/main/java/com/iexec/sms/tee/ConditionalOnTeeFramework.java b/src/main/java/com/iexec/sms/tee/ConditionalOnTeeFramework.java new file mode 100644 index 00000000..8f83b789 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/ConditionalOnTeeFramework.java @@ -0,0 +1,27 @@ +package com.iexec.sms.tee; + +import com.iexec.common.tee.TeeFramework; +import org.springframework.context.annotation.Conditional; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a way to include beans based on active profiles. + *

    + * E.g.: + *

      + *
    • a {@code SconeConfig} bean annotated with {@code ConditionalOnTeeFramework(frameworks = SCONE)} + * will be loaded only if a {@code scone} profile is active.
    • + *
    • a {@code TeeConfig} bean annotated with {@code ConditionalOnTeeFramework(frameworks = {SCONE, GRAMINE})} + * will be loaded only if any of {@code scone} or {@code gramine} profile is active.
    • + *
    + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Conditional(OnTeeFrameworkCondition.class) +public @interface ConditionalOnTeeFramework { + TeeFramework[] frameworks(); +} diff --git a/src/main/java/com/iexec/sms/tee/OnTeeFrameworkCondition.java b/src/main/java/com/iexec/sms/tee/OnTeeFrameworkCondition.java new file mode 100644 index 00000000..741215b6 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/OnTeeFrameworkCondition.java @@ -0,0 +1,72 @@ +package com.iexec.sms.tee; + +import com.iexec.common.tee.TeeFramework; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; + +import java.util.Arrays; +import java.util.Map; + +/** + * {@link Condition} that checks for a specific profile to be enabled. + * To be used with {@link ConditionalOnTeeFramework}. + */ +@Slf4j +public class OnTeeFrameworkCondition extends SpringBootCondition { + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + final Map attributes = metadata.getAnnotationAttributes(ConditionalOnTeeFramework.class.getName()); + final String[] activeProfiles = context.getEnvironment().getActiveProfiles(); + + final String beanClassName; + + if (metadata instanceof AnnotationMetadata) { + beanClassName = ((AnnotationMetadata) metadata).getClassName(); + } else { + beanClassName = ((MethodMetadata) metadata).getMethodName(); + } + + if (attributes == null) { + log.warn("No attribute for bean annotation, won't be loaded [bean:{}", + beanClassName); + return new ConditionOutcome( + false, + ConditionMessage.forCondition(ConditionalOnTeeFramework.class).didNotFind("any TEE frameworks").atAll()); + } + + final TeeFramework[] frameworks = (TeeFramework[]) attributes.get("frameworks"); + if (frameworks == null || frameworks.length == 0) { + log.warn( + "No TEE framework defined for bean, won't be loaded [bean:{}]", + beanClassName); + return new ConditionOutcome( + false, + ConditionMessage.forCondition(ConditionalOnTeeFramework.class).didNotFind("any TEE frameworks").atAll()); + } + + for (String activeProfile : activeProfiles) { + for (TeeFramework framework : frameworks) { + if (activeProfile.equalsIgnoreCase(framework.name())) { + return new ConditionOutcome( + true, + ConditionMessage.forCondition(ConditionalOnTeeFramework.class).foundExactly(framework)); + } + } + } + + log.debug( + "Active profiles and condition don't match, bean won't be loaded [bean:{}]", + beanClassName); + + return new ConditionOutcome( + false, + ConditionMessage.forCondition(ConditionalOnTeeFramework.class).didNotFind("profile", "profiles").items(Arrays.asList(frameworks))); + } +} diff --git a/src/main/java/com/iexec/sms/tee/TeeController.java b/src/main/java/com/iexec/sms/tee/TeeController.java index 917eb875..d52a468c 100644 --- a/src/main/java/com/iexec/sms/tee/TeeController.java +++ b/src/main/java/com/iexec/sms/tee/TeeController.java @@ -18,16 +18,17 @@ import com.iexec.common.chain.WorkerpoolAuthorization; -import com.iexec.sms.api.TeeSessionGenerationError; -import com.iexec.common.tee.TeeWorkflowSharedConfiguration; +import com.iexec.common.tee.TeeFramework; import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.api.TeeSessionGenerationResponse; +import com.iexec.sms.api.config.TeeServicesProperties; import com.iexec.sms.authorization.AuthorizationError; import com.iexec.sms.authorization.AuthorizationService; import com.iexec.sms.tee.challenge.TeeChallenge; import com.iexec.sms.tee.challenge.TeeChallengeService; -import com.iexec.sms.tee.session.TeeSessionGenerationException; import com.iexec.sms.tee.session.TeeSessionService; -import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -57,33 +58,45 @@ public class TeeController { private final AuthorizationService authorizationService; private final TeeChallengeService teeChallengeService; private final TeeSessionService teeSessionService; - private final TeeWorkflowConfiguration teeWorkflowConfig; + private final TeeServicesProperties teeServicesProperties; public TeeController( AuthorizationService authorizationService, TeeChallengeService teeChallengeService, TeeSessionService teeSessionService, - TeeWorkflowConfiguration teeWorkflowConfig) { + TeeServicesProperties teeServicesProperties) { this.authorizationService = authorizationService; this.teeChallengeService = teeChallengeService; this.teeSessionService = teeSessionService; - this.teeWorkflowConfig = teeWorkflowConfig; + this.teeServicesProperties = teeServicesProperties; } /** - * Retrieve configuration for tee workflow. This includes configuration - * for pre-compute and post-compute stages. - *

    - * Note: Being able to read the fingerprints on this endpoint is not required - * for the workflow but it might be convenient to keep it for - * transparency purposes. + * Return which TEE framework this SMS is configured to use. + * @return TEE framework this SMS is configured to use. + */ + @GetMapping("/framework") + public ResponseEntity getTeeFramework() { + return ResponseEntity.ok(teeServicesProperties.getTeeFramework()); + } + + /** + * Retrieve properties for TEE services. This includes properties + * for pre-compute and post-compute stages + * and potential TEE framework's specific data. * - * @return tee workflow config (pre-compute image uri, post-compute image uri, - * pre-compute fingerprint, heap size, ...) + * @return TEE services properties (pre-compute image uri, post-compute image uri, + * heap size, ...) */ - @GetMapping("/workflow/config") - public ResponseEntity getTeeWorkflowSharedConfig() { - return ResponseEntity.ok(teeWorkflowConfig.getSharedConfiguration()); + @GetMapping("/properties/{teeFramework}") + public ResponseEntity getTeeServicesProperties( + @PathVariable TeeFramework teeFramework) { + if (teeFramework != teeServicesProperties.getTeeFramework()) { + log.error("SMS configured to use another TeeFramework " + + "[required:{}, actual:{}]", teeFramework, teeServicesProperties.getTeeFramework()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + return ResponseEntity.ok(teeServicesProperties); } /** @@ -111,14 +124,14 @@ public ResponseEntity generateTeeChallenge(@PathVariable String chainTas * 500 INTERNAL_SERVER_ERROR otherwise. */ @PostMapping("/sessions") - public ResponseEntity> generateTeeSession( + public ResponseEntity> generateTeeSession( @RequestHeader("Authorization") String authorization, @RequestBody WorkerpoolAuthorization workerpoolAuthorization) { String workerAddress = workerpoolAuthorization.getWorkerWallet(); String challenge = authorizationService.getChallengeForWorker(workerpoolAuthorization); if (!authorizationService.isSignedByHimself(challenge, authorization, workerAddress)) { - final ApiResponseBody body = - ApiResponseBody.builder() + final ApiResponseBody body = + ApiResponseBody.builder() .error(INVALID_AUTHORIZATION) .build(); @@ -132,8 +145,8 @@ public ResponseEntity> genera final TeeSessionGenerationError teeSessionGenerationError = authorizationToGenerationError.get(authorizationError.get()); - final ApiResponseBody body = - ApiResponseBody.builder() + final ApiResponseBody body = + ApiResponseBody.builder() .error(teeSessionGenerationError) .build(); @@ -147,19 +160,21 @@ public ResponseEntity> genera log.info("TEE session request [taskId:{}, workerAddress:{}]", taskId, workerAddress); try { - String sessionId = teeSessionService + TeeSessionGenerationResponse teeSessionGenerationResponse = teeSessionService .generateTeeSession(taskId, workerAddress, attestingEnclave); - if (sessionId.isEmpty()) { + if (teeSessionGenerationResponse == null) { return ResponseEntity.notFound().build(); } - return ResponseEntity.ok(ApiResponseBody.builder().data(sessionId).build()); + return ResponseEntity.ok(ApiResponseBody.builder() + .data(teeSessionGenerationResponse) + .build()); } catch(TeeSessionGenerationException e) { log.error("Failed to generate secure session [taskId:{}, workerAddress:{}]", taskId, workerAddress, e); - final ApiResponseBody body = - ApiResponseBody.builder() + final ApiResponseBody body = + ApiResponseBody.builder() .error(e.getError()) .build(); return ResponseEntity @@ -168,8 +183,8 @@ public ResponseEntity> genera } catch (Exception e) { log.error("Failed to generate secure session with unknown reason [taskId:{}, workerAddress:{}]", taskId, workerAddress, e); - final ApiResponseBody body = - ApiResponseBody.builder() + final ApiResponseBody body = + ApiResponseBody.builder() .error(SECURE_SESSION_GENERATION_FAILED) .build(); return ResponseEntity diff --git a/src/main/java/com/iexec/sms/tee/TeeFrameworkConverter.java b/src/main/java/com/iexec/sms/tee/TeeFrameworkConverter.java new file mode 100644 index 00000000..78ccd54e --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/TeeFrameworkConverter.java @@ -0,0 +1,17 @@ +package com.iexec.sms.tee; + +import com.iexec.common.tee.TeeFramework; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +/** + * This class is needed to have a case-insensitive `teeFramework` path variable in + * {@link TeeController#getTeeServicesProperties(TeeFramework)}. + */ +@Component +public class TeeFrameworkConverter implements Converter { + @Override + public TeeFramework convert(String value) { + return TeeFramework.valueOf(value.toUpperCase()); + } +} diff --git a/src/main/java/com/iexec/sms/tee/challenge/TeeChallengeService.java b/src/main/java/com/iexec/sms/tee/challenge/TeeChallengeService.java index 992db397..551e0c61 100644 --- a/src/main/java/com/iexec/sms/tee/challenge/TeeChallengeService.java +++ b/src/main/java/com/iexec/sms/tee/challenge/TeeChallengeService.java @@ -29,8 +29,8 @@ @Service public class TeeChallengeService { - private TeeChallengeRepository teeChallengeRepository; - private EncryptionService encryptionService; + private final TeeChallengeRepository teeChallengeRepository; + private final EncryptionService encryptionService; public TeeChallengeService(TeeChallengeRepository teeChallengeRepository, EncryptionService encryptionService) { diff --git a/src/main/java/com/iexec/sms/tee/config/TeeWorkerInternalConfiguration.java b/src/main/java/com/iexec/sms/tee/config/TeeWorkerInternalConfiguration.java new file mode 100644 index 00000000..e907ba26 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/config/TeeWorkerInternalConfiguration.java @@ -0,0 +1,82 @@ +package com.iexec.sms.tee.config; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.config.GramineServicesProperties; +import com.iexec.sms.api.config.SconeServicesProperties; +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.unit.DataSize; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Positive; + +@Configuration +@Validated +public class TeeWorkerInternalConfiguration { + @Bean + TeeAppProperties preComputeProperties( + @Value("${tee.worker.pre-compute.image}") + @NotBlank(message = "pre-compute image must be provided") + String preComputeImage, + @Value("${tee.worker.pre-compute.fingerprint}") + @NotBlank(message = "pre-compute fingerprint must be provided") + String preComputeFingerprint, + @Value("${tee.worker.pre-compute.entrypoint}") + @NotBlank(message = "pre-compute entrypoint must be provided") + String preComputeEntrypoint, + @Value("${tee.worker.pre-compute.heap-size-gb}") + @Positive(message = "pre-compute heap size must be provided") + long preComputeHeapSizeInGB) { + return new TeeAppProperties( + preComputeImage, + preComputeFingerprint, + preComputeEntrypoint, + DataSize.ofGigabytes(preComputeHeapSizeInGB).toBytes() + ); + } + + @Bean + TeeAppProperties postComputeProperties( + @Value("${tee.worker.post-compute.image}") + @NotBlank(message = "post-compute image must be provided") + String postComputeImage, + @Value("${tee.worker.post-compute.fingerprint}") + @NotBlank(message = "post-compute fingerprint must be provided") + String postComputeFingerprint, + @Value("${tee.worker.post-compute.entrypoint}") + @NotBlank(message = "post-compute entrypoint must be provided") + String postComputeEntrypoint, + @Value("${tee.worker.post-compute.heap-size-gb}") + @Positive(message = "post-compute heap size must be provided") + long postComputeHeapSizeInGB) { + return new TeeAppProperties( + postComputeImage, + postComputeFingerprint, + postComputeEntrypoint, + DataSize.ofGigabytes(postComputeHeapSizeInGB).toBytes() + ); + } + + @Bean + @ConditionalOnTeeFramework(frameworks = TeeFramework.GRAMINE) + GramineServicesProperties gramineServicesProperties( + TeeAppProperties preComputeProperties, + TeeAppProperties postComputeProperties) { + return new GramineServicesProperties(preComputeProperties, postComputeProperties); + } + + @Bean + @ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) + SconeServicesProperties sconeServicesProperties( + TeeAppProperties preComputeProperties, + TeeAppProperties postComputeProperties, + @Value("${tee.scone.las-image}") + @NotBlank(message = "las image must be provided") + String lasImage) { + return new SconeServicesProperties(preComputeProperties, postComputeProperties, lasImage); + } +} diff --git a/src/main/java/com/iexec/sms/tee/session/TeeSessionLogConfiguration.java b/src/main/java/com/iexec/sms/tee/session/TeeSessionLogConfiguration.java new file mode 100644 index 00000000..535390c2 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/TeeSessionLogConfiguration.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import lombok.Getter; + +@Getter +@Component +public class TeeSessionLogConfiguration { + + @Value("${logging.tee.display-debug-session}") + boolean displayDebugSessionEnabled; +} diff --git a/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java b/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java index 22f552ea..b2b26390 100644 --- a/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java +++ b/src/main/java/com/iexec/sms/tee/session/TeeSessionService.java @@ -17,83 +17,65 @@ package com.iexec.sms.tee.session; import com.iexec.common.task.TaskDescription; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.TeeSessionGenerationResponse; import com.iexec.sms.blockchain.IexecHubService; -import com.iexec.sms.tee.session.cas.CasClient; -import com.iexec.sms.tee.session.palaemon.PalaemonSessionRequest; -import com.iexec.sms.tee.session.palaemon.PalaemonSessionService; -import lombok.extern.slf4j.Slf4j; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionHandler; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; import org.apache.commons.lang3.RandomStringUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import static com.iexec.sms.api.TeeSessionGenerationError.*; +import static com.iexec.sms.api.TeeSessionGenerationError.GET_TASK_DESCRIPTION_FAILED; +import static com.iexec.sms.api.TeeSessionGenerationError.SECURE_SESSION_NO_TEE_PROVIDER; -@Slf4j @Service public class TeeSessionService { private final IexecHubService iexecHubService; - private final CasClient casClient; - private final PalaemonSessionService palaemonSessionService; - private final boolean shouldDisplayDebugSession; + private final TeeSessionHandler teeSessionHandler; + public TeeSessionService( IexecHubService iexecService, - PalaemonSessionService palaemonSessionService, - CasClient casClient, - @Value("${logging.tee.display-debug-session}") - boolean shouldDisplayDebugSession) { + TeeSessionHandler teeSessionHandler) { this.iexecHubService = iexecService; - this.palaemonSessionService = palaemonSessionService; - this.casClient = casClient; - this.shouldDisplayDebugSession = shouldDisplayDebugSession; + this.teeSessionHandler = teeSessionHandler; } - public String generateTeeSession( + public TeeSessionGenerationResponse generateTeeSession( String taskId, String workerAddress, String teeChallenge) throws TeeSessionGenerationException { - String sessionId = createSessionId(taskId); TaskDescription taskDescription = iexecHubService.getTaskDescription(taskId); if (taskDescription == null) { throw new TeeSessionGenerationException( GET_TASK_DESCRIPTION_FAILED, - String.format("Failed to get task description [taskId:%s]", taskId) - ); + String.format("Failed to get task description [taskId:%s]", taskId)); } - PalaemonSessionRequest request = PalaemonSessionRequest.builder() + TeeSessionRequest request = TeeSessionRequest.builder() .sessionId(sessionId) .taskDescription(taskDescription) .workerAddress(workerAddress) .enclaveChallenge(teeChallenge) .build(); - String sessionYmlAsString = palaemonSessionService.getSessionYml(request); - if (sessionYmlAsString.isEmpty()) { + + final TeeFramework teeFramework = taskDescription.getTeeFramework(); + if (teeFramework == null) { throw new TeeSessionGenerationException( - GET_SESSION_YML_FAILED, - String.format("Failed to get session yml [taskId:%s, workerAddress:%s]", taskId, workerAddress)); - } - log.info("Session yml is ready [taskId:{}]", taskId); - if (shouldDisplayDebugSession){ - log.info("Session yml content [taskId:{}]\n{}", taskId, sessionYmlAsString); + SECURE_SESSION_NO_TEE_PROVIDER, + String.format("TEE framework can't be null [taskId:%s]", taskId)); } + // /!\ TODO clean expired tasks sessions - boolean isSessionGenerated = casClient - .generateSecureSession(sessionYmlAsString.getBytes()) - .getStatusCode() - .is2xxSuccessful(); - if (!isSessionGenerated) { - throw new TeeSessionGenerationException( - SECURE_SESSION_CAS_CALL_FAILED, - String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", taskId, workerAddress) - ); - } - return sessionId; + String secretProvisioningUrl = teeSessionHandler.buildAndPostSession(request); + return new TeeSessionGenerationResponse(sessionId, secretProvisioningUrl); } private String createSessionId(String taskId) { String randomString = RandomStringUtils.randomAlphanumeric(10); return String.format("%s0000%s", randomString, taskId); } + } diff --git a/src/main/java/com/iexec/sms/tee/session/base/SecretEnclaveBase.java b/src/main/java/com/iexec/sms/tee/session/base/SecretEnclaveBase.java new file mode 100644 index 00000000..6dc934b7 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/base/SecretEnclaveBase.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.base; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.Map; + +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class SecretEnclaveBase { + + @JsonProperty("name") + private String name; + @JsonProperty("mrenclave") + private String mrenclave; + @JsonProperty("environment") + private Map environment; + +} diff --git a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsRepository.java b/src/main/java/com/iexec/sms/tee/session/base/SecretSessionBase.java similarity index 63% rename from src/main/java/com/iexec/sms/secret/web2/Web2SecretsRepository.java rename to src/main/java/com/iexec/sms/tee/session/base/SecretSessionBase.java index 062ae118..52c68889 100644 --- a/src/main/java/com/iexec/sms/secret/web2/Web2SecretsRepository.java +++ b/src/main/java/com/iexec/sms/tee/session/base/SecretSessionBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2022 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,19 @@ * limitations under the License. */ -package com.iexec.sms.secret.web2; +package com.iexec.sms.tee.session.base; +import lombok.*; -import org.springframework.data.repository.CrudRepository; +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class SecretSessionBase { -import java.util.Optional; - -public interface Web2SecretsRepository extends CrudRepository { - - Optional findWeb2SecretsByOwnerAddress(String ownerAddress); + private SecretEnclaveBase preCompute; + private SecretEnclaveBase appCompute; + private SecretEnclaveBase postCompute; } diff --git a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java b/src/main/java/com/iexec/sms/tee/session/base/SecretSessionBaseService.java similarity index 54% rename from src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java rename to src/main/java/com/iexec/sms/tee/session/base/SecretSessionBaseService.java index f7a29767..c6d561d1 100644 --- a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionService.java +++ b/src/main/java/com/iexec/sms/tee/session/base/SecretSessionBaseService.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2022 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,167 +14,106 @@ * limitations under the License. */ -package com.iexec.sms.tee.session.palaemon; +package com.iexec.sms.tee.session.base; import com.iexec.common.task.TaskDescription; import com.iexec.common.tee.TeeEnclaveConfiguration; -import com.iexec.common.utils.FileHelper; import com.iexec.common.utils.IexecEnvUtils; -import com.iexec.sms.secret.ReservedSecretKeyName; -import com.iexec.sms.secret.Secret; +import com.iexec.common.utils.IexecFileHelper; +import com.iexec.sms.api.config.TeeServicesProperties; import com.iexec.sms.secret.compute.OnChainObjectType; import com.iexec.sms.secret.compute.SecretOwnerRole; import com.iexec.sms.secret.compute.TeeTaskComputeSecret; import com.iexec.sms.secret.compute.TeeTaskComputeSecretService; -import com.iexec.sms.secret.web2.Web2SecretsService; +import com.iexec.sms.secret.web2.Web2SecretService; import com.iexec.sms.secret.web3.Web3SecretService; import com.iexec.sms.tee.challenge.TeeChallenge; import com.iexec.sms.tee.challenge.TeeChallengeService; -import com.iexec.sms.tee.session.TeeSessionGenerationException; -import com.iexec.sms.tee.session.attestation.AttestationSecurityConfig; -import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; +import com.iexec.sms.tee.session.base.SecretEnclaveBase.SecretEnclaveBaseBuilder; +import com.iexec.sms.tee.session.base.SecretSessionBase.SecretSessionBaseBuilder; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; import com.iexec.sms.utils.EthereumCredentials; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import javax.annotation.PostConstruct; -import java.io.FileNotFoundException; -import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; +import java.util.*; import static com.iexec.common.chain.DealParams.DROPBOX_RESULT_STORAGE_PROVIDER; -import static com.iexec.common.precompute.PreComputeUtils.IEXEC_DATASET_KEY; import static com.iexec.common.precompute.PreComputeUtils.IS_DATASET_REQUIRED; import static com.iexec.common.tee.TeeUtils.booleanToYesNo; import static com.iexec.common.worker.result.ResultUtils.*; import static com.iexec.sms.api.TeeSessionGenerationError.*; -import static com.iexec.sms.secret.ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY; +import static com.iexec.sms.secret.ReservedSecretKeyName.*; @Slf4j @Service -public class PalaemonSessionService { +public class SecretSessionBaseService { - public static final String EMPTY_YML_VALUE = ""; + static final String EMPTY_YML_VALUE = ""; - // Internal values required for setting up a palaemon session - // Generic - static final String SESSION_ID = "SESSION_ID"; - static final String INPUT_FILE_URLS = "INPUT_FILE_URLS"; - static final String INPUT_FILE_NAMES = "INPUT_FILE_NAMES"; - static final String TOLERATED_INSECURE_OPTIONS = "TOLERATED_INSECURE_OPTIONS"; - static final String IGNORED_SGX_ADVISORIES = "IGNORED_SGX_ADVISORIES"; + public static final String INPUT_FILE_URLS = "INPUT_FILE_URLS"; + public static final String INPUT_FILE_NAMES = "INPUT_FILE_NAMES"; // PreCompute static final String IS_PRE_COMPUTE_REQUIRED = "IS_PRE_COMPUTE_REQUIRED"; - static final String PRE_COMPUTE_MRENCLAVE = "PRE_COMPUTE_MRENCLAVE"; - static final String PRE_COMPUTE_ENTRYPOINT = "PRE_COMPUTE_ENTRYPOINT"; + public static final String PRE_COMPUTE_MRENCLAVE = "PRE_COMPUTE_MRENCLAVE"; + static final String IEXEC_PRE_COMPUTE_OUT = "IEXEC_PRE_COMPUTE_OUT"; + static final String IEXEC_DATASET_KEY = "IEXEC_DATASET_KEY"; // Compute - static final String APP_MRENCLAVE = "APP_MRENCLAVE"; - static final String APP_ARGS = "APP_ARGS"; + public static final String APP_MRENCLAVE = "APP_MRENCLAVE"; // PostCompute - static final String POST_COMPUTE_MRENCLAVE = "POST_COMPUTE_MRENCLAVE"; - static final String POST_COMPUTE_ENTRYPOINT = "POST_COMPUTE_ENTRYPOINT"; - // Secrets - static final String REQUESTER_SECRETS = "REQUESTER_SECRETS"; - // Env - private static final String ENV_PROPERTY = "env"; + public static final String POST_COMPUTE_MRENCLAVE = "POST_COMPUTE_MRENCLAVE"; private final Web3SecretService web3SecretService; - private final Web2SecretsService web2SecretsService; + private final Web2SecretService web2SecretService; private final TeeChallengeService teeChallengeService; - private final TeeWorkflowConfiguration teeWorkflowConfig; - private final AttestationSecurityConfig attestationSecurityConfig; + private final TeeServicesProperties teeServicesConfig; private final TeeTaskComputeSecretService teeTaskComputeSecretService; - @Value("${scone.cas.palaemon}") - private String palaemonTemplateFilePath; - - public PalaemonSessionService( + public SecretSessionBaseService( Web3SecretService web3SecretService, - Web2SecretsService web2SecretsService, + Web2SecretService web2SecretService, TeeChallengeService teeChallengeService, - TeeWorkflowConfiguration teeWorkflowConfig, - AttestationSecurityConfig attestationSecurityConfig, + TeeServicesProperties teeServicesConfig, TeeTaskComputeSecretService teeTaskComputeSecretService) { this.web3SecretService = web3SecretService; - this.web2SecretsService = web2SecretsService; + this.web2SecretService = web2SecretService; this.teeChallengeService = teeChallengeService; - this.teeWorkflowConfig = teeWorkflowConfig; - this.attestationSecurityConfig = attestationSecurityConfig; + this.teeServicesConfig = teeServicesConfig; this.teeTaskComputeSecretService = teeTaskComputeSecretService; } - @PostConstruct - void postConstruct() throws FileNotFoundException { - if (StringUtils.isEmpty(palaemonTemplateFilePath)) { - throw new IllegalArgumentException("Missing palaemon template filepath"); - } - if (!FileHelper.exists(palaemonTemplateFilePath)) { - throw new FileNotFoundException("Missing palaemon template file"); - } - } - /** - * Collect tokens required for different compute stages (pre, in, post) - * and build the yaml config of the TEE session. - *

    - * TODO: Read onchain available infos from enclave instead of copying - * public vars to palaemon.yml. It needs ssl call from enclave to eth - * node (only ethereum node address required inside palaemon.yml) + * Collect tokens required for different compute stages (pre, in, post). * * @param request session request details - * @return session config in yaml string format + * @return All common tokens for a session, whatever TEE technology is used */ - public String getSessionYml(PalaemonSessionRequest request) throws TeeSessionGenerationException { + public SecretSessionBase getSecretsTokens(TeeSessionRequest request) throws TeeSessionGenerationException { if (request == null) { throw new TeeSessionGenerationException( NO_SESSION_REQUEST, - "Session request must not be null" - ); + "Session request must not be null"); } if (request.getTaskDescription() == null) { throw new TeeSessionGenerationException( NO_TASK_DESCRIPTION, - "Task description must not be null" - ); + "Task description must not be null"); } - + SecretSessionBaseBuilder sessionBase = SecretSessionBase.builder(); TaskDescription taskDescription = request.getTaskDescription(); - Map palaemonTokens = new HashMap<>(); - palaemonTokens.put(SESSION_ID, request.getSessionId()); // pre-compute boolean isPreComputeRequired = taskDescription.containsDataset() || !taskDescription.getInputFiles().isEmpty(); - palaemonTokens.put(IS_PRE_COMPUTE_REQUIRED, isPreComputeRequired); if (isPreComputeRequired) { - palaemonTokens.putAll(getPreComputePalaemonTokens(request)); + sessionBase.preCompute(getPreComputeTokens(request)); } // app - palaemonTokens.putAll(getAppPalaemonTokens(request)); + sessionBase.appCompute(getAppTokens(request)); // post compute - palaemonTokens.putAll(getPostComputePalaemonTokens(request)); - // env variables - Map env = IexecEnvUtils.getAllIexecEnv(taskDescription); - // Null value should be replaced by an empty string. - env.forEach((key, value) -> env.replace(key, null, EMPTY_YML_VALUE)); - palaemonTokens.put(ENV_PROPERTY, env); - // Add attestation security config - String toleratedInsecureOptions = - String.join(",", attestationSecurityConfig.getToleratedInsecureOptions()); - String ignoredSgxAdvisories = - String.join(",", attestationSecurityConfig.getIgnoredSgxAdvisories()); - palaemonTokens.put(TOLERATED_INSECURE_OPTIONS, toleratedInsecureOptions); - palaemonTokens.put(IGNORED_SGX_ADVISORIES, ignoredSgxAdvisories); - // Merge template with tokens and return the result - return getFilledPalaemonTemplate(this.palaemonTemplateFilePath, palaemonTokens); + sessionBase.postCompute(getPostComputeTokens(request)); + return sessionBase.build(); } /** @@ -183,51 +122,63 @@ public String getSessionYml(PalaemonSessionRequest request) throws TeeSessionGen * @return map of pre-compute tokens * @throws TeeSessionGenerationException if dataset secret is not found. */ - Map getPreComputePalaemonTokens(PalaemonSessionRequest request) + public SecretEnclaveBase getPreComputeTokens(TeeSessionRequest request) throws TeeSessionGenerationException { + SecretEnclaveBaseBuilder enclaveBase = SecretEnclaveBase.builder(); + enclaveBase.name("pre-compute"); + Map tokens = new HashMap<>(); TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); - Map tokens = new HashMap<>(); - String fingerprint = teeWorkflowConfig.getPreComputeFingerprint(); - tokens.put(PRE_COMPUTE_MRENCLAVE, fingerprint); - String entrypoint = teeWorkflowConfig.getPreComputeEntrypoint(); - tokens.put(PRE_COMPUTE_ENTRYPOINT, entrypoint); + enclaveBase.mrenclave(teeServicesConfig.getPreComputeProperties().getFingerprint()); + tokens.put(IEXEC_PRE_COMPUTE_OUT, IexecFileHelper.SLASH_IEXEC_IN); + // `IS_DATASET_REQUIRED` still meaningful? tokens.put(IS_DATASET_REQUIRED, taskDescription.containsDataset()); - tokens.put(IEXEC_DATASET_KEY, EMPTY_YML_VALUE); + + List trustedEnv = new ArrayList<>(); if (taskDescription.containsDataset()) { String datasetKey = web3SecretService - .getSecret(taskDescription.getDatasetAddress(), true) + .getDecryptedValue(taskDescription.getDatasetAddress()) .orElseThrow(() -> new TeeSessionGenerationException( PRE_COMPUTE_GET_DATASET_SECRET_FAILED, - "Empty dataset secret - taskId: " + taskId - )) - .getTrimmedValue(); + "Empty dataset secret - taskId: " + taskId)); tokens.put(IEXEC_DATASET_KEY, datasetKey); + trustedEnv.addAll(List.of( + IexecEnvUtils.IEXEC_DATASET_URL, + IexecEnvUtils.IEXEC_DATASET_FILENAME, + IexecEnvUtils.IEXEC_DATASET_CHECKSUM)); } else { log.info("No dataset key needed for this task [taskId:{}]", taskId); } - // extract - // this map will be empty (not null) if no input file is found - Map inputFileUrls = IexecEnvUtils.getAllIexecEnv(taskDescription) + trustedEnv.addAll(List.of( + IexecEnvUtils.IEXEC_TASK_ID, + IexecEnvUtils.IEXEC_INPUT_FILES_FOLDER, + IexecEnvUtils.IEXEC_INPUT_FILES_NUMBER)); + IexecEnvUtils.getAllIexecEnv(taskDescription) .entrySet() .stream() - .filter(e -> e.getKey().contains(IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - tokens.put(INPUT_FILE_URLS, inputFileUrls); - return tokens; + .filter(e -> + // extract trusted en vars to include + trustedEnv.contains(e.getKey()) + // extract + || e.getKey().startsWith(IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX)) + .forEach(e -> tokens.put(e.getKey(), e.getValue())); + return enclaveBase + .environment(tokens) + .build(); } /* * Compute (App) */ - Map getAppPalaemonTokens(PalaemonSessionRequest request) - throws TeeSessionGenerationException{ + public SecretEnclaveBase getAppTokens(TeeSessionRequest request) + throws TeeSessionGenerationException { + SecretEnclaveBaseBuilder enclaveBase = SecretEnclaveBase.builder(); + enclaveBase.name("app"); TaskDescription taskDescription = request.getTaskDescription(); if (taskDescription == null) { throw new TeeSessionGenerationException( NO_TASK_DESCRIPTION, - "Task description must no be null" - ); + "Task description must not be null"); } Map tokens = new HashMap<>(); @@ -235,36 +186,31 @@ Map getAppPalaemonTokens(PalaemonSessionRequest request) if (enclaveConfig == null) { throw new TeeSessionGenerationException( APP_COMPUTE_NO_ENCLAVE_CONFIG, - "Enclave configuration must no be null" - ); + "Enclave configuration must not be null"); } - if (!enclaveConfig.getValidator().isValid()){ + if (!enclaveConfig.getValidator().isValid()) { throw new TeeSessionGenerationException( APP_COMPUTE_INVALID_ENCLAVE_CONFIG, "Invalid enclave configuration: " + - enclaveConfig.getValidator().validate().toString() - ); + enclaveConfig.getValidator().validate().toString()); } - tokens.put(APP_MRENCLAVE, enclaveConfig.getFingerprint()); - String appArgs = enclaveConfig.getEntrypoint(); - if (!StringUtils.isEmpty(taskDescription.getCmd())) { - appArgs = appArgs + " " + taskDescription.getCmd(); - } - tokens.put(APP_ARGS, appArgs); + enclaveBase.mrenclave(enclaveConfig.getFingerprint()); // extract // this map will be empty (not null) if no input file is found - Map inputFileNames = IexecEnvUtils.getComputeStageEnvMap(taskDescription) + IexecEnvUtils.getComputeStageEnvMap(taskDescription) .entrySet() .stream() - .filter(e -> e.getKey().contains(IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - tokens.put(INPUT_FILE_NAMES, inputFileNames); + .filter(e -> e.getKey().startsWith(IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX)) + .forEach(e -> tokens.put(e.getKey(), e.getValue())); final Map computeSecrets = getApplicationComputeSecrets(taskDescription); tokens.putAll(computeSecrets); - - return tokens; + // trusted env variables (not confidential) + tokens.putAll(IexecEnvUtils.getComputeStageEnvMap(taskDescription)); + return enclaveBase + .environment(tokens) + .build(); } private Map getApplicationComputeSecrets(TaskDescription taskDescription) { @@ -273,25 +219,26 @@ private Map getApplicationComputeSecrets(TaskDescription taskDes if (applicationAddress != null) { final String secretIndex = "1"; - String appDeveloperSecret = - teeTaskComputeSecretService.getSecret( - OnChainObjectType.APPLICATION, - applicationAddress.toLowerCase(), - SecretOwnerRole.APPLICATION_DEVELOPER, - "", - secretIndex) - .map(TeeTaskComputeSecret::getValue) - .orElse(EMPTY_YML_VALUE); - tokens.put(IexecEnvUtils.IEXEC_APP_DEVELOPER_SECRET_PREFIX + secretIndex, appDeveloperSecret); + String appDeveloperSecret = teeTaskComputeSecretService.getSecret( + OnChainObjectType.APPLICATION, + applicationAddress.toLowerCase(), + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + secretIndex) + .map(TeeTaskComputeSecret::getValue) + .orElse(EMPTY_YML_VALUE); + if (!StringUtils.isEmpty(appDeveloperSecret)) { + tokens.put("IEXEC_APP_DEVELOPER_SECRET", appDeveloperSecret); + tokens.put(IexecEnvUtils.IEXEC_APP_DEVELOPER_SECRET_PREFIX + secretIndex, appDeveloperSecret); + } } if (taskDescription.getSecrets() == null || taskDescription.getRequester() == null) { - tokens.put(REQUESTER_SECRETS, Collections.emptyMap()); return tokens; } final HashMap requesterSecrets = new HashMap<>(); - for (Map.Entry secretEntry: taskDescription.getSecrets().entrySet()) { + for (Map.Entry secretEntry : taskDescription.getSecrets().entrySet()) { try { int requesterSecretIndex = Integer.parseInt(secretEntry.getKey()); if (requesterSecretIndex <= 0) { @@ -300,49 +247,47 @@ private Map getApplicationComputeSecrets(TaskDescription taskDes log.warn(message); throw new NumberFormatException(message); } - } catch(NumberFormatException e) { + } catch (NumberFormatException e) { log.warn("Invalid entry found in deal parameters secrets map", e); continue; } String requesterSecret = teeTaskComputeSecretService.getSecret( - OnChainObjectType.APPLICATION, - "", - SecretOwnerRole.REQUESTER, - taskDescription.getRequester().toLowerCase(), - secretEntry.getValue()) + OnChainObjectType.APPLICATION, + "", + SecretOwnerRole.REQUESTER, + taskDescription.getRequester().toLowerCase(), + secretEntry.getValue()) .map(TeeTaskComputeSecret::getValue) .orElse(EMPTY_YML_VALUE); requesterSecrets.put(IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + secretEntry.getKey(), requesterSecret); } - tokens.put(REQUESTER_SECRETS, requesterSecrets); - + tokens.putAll(requesterSecrets); return tokens; } /* * Post-Compute (Result) */ - Map getPostComputePalaemonTokens(PalaemonSessionRequest request) + public SecretEnclaveBase getPostComputeTokens(TeeSessionRequest request) throws TeeSessionGenerationException { + SecretEnclaveBaseBuilder enclaveBase = SecretEnclaveBase.builder() + .name("post-compute") + .mrenclave(teeServicesConfig.getPostComputeProperties().getFingerprint()); + Map tokens = new HashMap<>(); TaskDescription taskDescription = request.getTaskDescription(); if (taskDescription == null) { throw new TeeSessionGenerationException(NO_TASK_DESCRIPTION, "Task description must not be null"); } - - Map tokens = new HashMap<>(); - String teePostComputeFingerprint = teeWorkflowConfig.getPostComputeFingerprint(); // ############################################################################### // TODO: activate this when user specific post-compute is properly - // supported. See https://github.com/iExecBlockchainComputing/iexec-sms/issues/52. + // supported. See + // https://github.com/iExecBlockchainComputing/iexec-sms/issues/52. // ############################################################################### // // Use specific post-compute image if requested. - //if (taskDescription.containsPostCompute()) { - // teePostComputeFingerprint = taskDescription.getTeePostComputeFingerprint(); - // //add entrypoint too - //} - tokens.put(POST_COMPUTE_MRENCLAVE, teePostComputeFingerprint); - String entrypoint = teeWorkflowConfig.getPostComputeEntrypoint(); - tokens.put(POST_COMPUTE_ENTRYPOINT, entrypoint); + // if (taskDescription.containsPostCompute()) { + // teePostComputeFingerprint = taskDescription.getTeePostComputeFingerprint(); + // //add entrypoint too + // } // encryption Map encryptionTokens = getPostComputeEncryptionTokens(request); tokens.putAll(encryptionTokens); @@ -352,10 +297,12 @@ Map getPostComputePalaemonTokens(PalaemonSessionRequest request) // enclave signature Map signTokens = getPostComputeSignTokens(request); tokens.putAll(signTokens); - return tokens; + return enclaveBase + .environment(tokens) + .build(); } - Map getPostComputeEncryptionTokens(PalaemonSessionRequest request) + public Map getPostComputeEncryptionTokens(TeeSessionRequest request) throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); @@ -367,17 +314,15 @@ Map getPostComputeEncryptionTokens(PalaemonSessionRequest reques if (!shouldEncrypt) { return tokens; } - Optional beneficiaryResultEncryptionKeySecret = web2SecretsService.getSecret( + Optional beneficiaryResultEncryptionKeySecret = web2SecretService.getDecryptedValue( taskDescription.getBeneficiary(), - IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, - true); + IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY); if (beneficiaryResultEncryptionKeySecret.isEmpty()) { throw new TeeSessionGenerationException( POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, - "Empty beneficiary encryption key - taskId: " + taskId - ); + "Empty beneficiary encryption key - taskId: " + taskId); } - String publicKeyValue = beneficiaryResultEncryptionKeySecret.get().getTrimmedValue(); + String publicKeyValue = beneficiaryResultEncryptionKeySecret.get(); tokens.put(RESULT_ENCRYPTION_PUBLIC_KEY, publicKeyValue); // base64 encoded by client return tokens; } @@ -386,7 +331,7 @@ Map getPostComputeEncryptionTokens(PalaemonSessionRequest reques // to the beneficiary private storage space waiting for // that feature we only allow to push to the requester // private storage space - Map getPostComputeStorageTokens(PalaemonSessionRequest request) + public Map getPostComputeStorageTokens(TeeSessionRequest request) throws TeeSessionGenerationException { TaskDescription taskDescription = request.getTaskDescription(); String taskId = taskDescription.getChainTaskId(); @@ -402,10 +347,11 @@ Map getPostComputeStorageTokens(PalaemonSessionRequest request) String storageProvider = taskDescription.getResultStorageProvider(); String storageProxy = taskDescription.getResultStorageProxy(); String keyName = storageProvider.equals(DROPBOX_RESULT_STORAGE_PROVIDER) - ? ReservedSecretKeyName.IEXEC_RESULT_DROPBOX_TOKEN - : ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN; - Optional requesterStorageTokenSecret = - web2SecretsService.getSecret(taskDescription.getRequester(), keyName, true); + ? IEXEC_RESULT_DROPBOX_TOKEN + : IEXEC_RESULT_IEXEC_IPFS_TOKEN; + Optional requesterStorageTokenSecret = web2SecretService.getDecryptedValue( + taskDescription.getRequester(), + keyName); if (requesterStorageTokenSecret.isEmpty()) { log.error("Failed to get storage token [taskId:{}, storageProvider:{}, requester:{}]", taskId, storageProvider, taskDescription.getRequester()); @@ -413,14 +359,14 @@ Map getPostComputeStorageTokens(PalaemonSessionRequest request) POST_COMPUTE_GET_STORAGE_TOKENS_FAILED, "Empty requester storage token - taskId: " + taskId); } - String requesterStorageToken = requesterStorageTokenSecret.get().getTrimmedValue(); + String requesterStorageToken = requesterStorageTokenSecret.get(); tokens.put(RESULT_STORAGE_PROVIDER, storageProvider); tokens.put(RESULT_STORAGE_PROXY, storageProxy); tokens.put(RESULT_STORAGE_TOKEN, requesterStorageToken); return tokens; } - Map getPostComputeSignTokens(PalaemonSessionRequest request) + public Map getPostComputeSignTokens(TeeSessionRequest request) throws TeeSessionGenerationException { String taskId = request.getTaskDescription().getChainTaskId(); String workerAddress = request.getWorkerAddress(); @@ -428,28 +374,24 @@ Map getPostComputeSignTokens(PalaemonSessionRequest request) if (StringUtils.isEmpty(workerAddress)) { throw new TeeSessionGenerationException( POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS, - "Empty worker address - taskId: " + taskId - ); + "Empty worker address - taskId: " + taskId); } if (StringUtils.isEmpty(request.getEnclaveChallenge())) { throw new TeeSessionGenerationException( POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE, - "Empty public enclave challenge - taskId: " + taskId - ); + "Empty public enclave challenge - taskId: " + taskId); } Optional teeChallenge = teeChallengeService.getOrCreate(taskId, true); if (teeChallenge.isEmpty()) { throw new TeeSessionGenerationException( POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE, - "Empty TEE challenge - taskId: " + taskId - ); + "Empty TEE challenge - taskId: " + taskId); } EthereumCredentials enclaveCredentials = teeChallenge.get().getCredentials(); if (enclaveCredentials == null || enclaveCredentials.getPrivateKey().isEmpty()) { throw new TeeSessionGenerationException( POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS, - "Empty TEE challenge credentials - taskId: " + taskId - ); + "Empty TEE challenge credentials - taskId: " + taskId); } tokens.put(RESULT_TASK_ID, taskId); tokens.put(RESULT_SIGN_WORKER_ADDRESS, workerAddress); @@ -457,14 +399,4 @@ Map getPostComputeSignTokens(PalaemonSessionRequest request) return tokens; } - private String getFilledPalaemonTemplate(String templatePath, Map tokens) { - VelocityEngine ve = new VelocityEngine(); - ve.init(); - Template template = ve.getTemplate(templatePath); - VelocityContext context = new VelocityContext(); - tokens.forEach(context::put); // copy all data from the tokens into context - StringWriter writer = new StringWriter(); - template.merge(context, writer); - return writer.toString(); - } } diff --git a/src/main/java/com/iexec/sms/tee/session/cas/CasConfigurationController.java b/src/main/java/com/iexec/sms/tee/session/cas/CasConfigurationController.java deleted file mode 100644 index cfe8dfd5..00000000 --- a/src/main/java/com/iexec/sms/tee/session/cas/CasConfigurationController.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2021 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.tee.session.cas; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/cas") -public class CasConfigurationController { - - private final CasConfiguration casConfiguration; - - public CasConfigurationController(CasConfiguration casConfiguration) { - this.casConfiguration = casConfiguration; - } - - /** - * Get CAS public url intended for enclaves. - * - * @return enclave dedicated url - */ - @GetMapping("/url") - public String getCasEnclaveUrl() { - return casConfiguration.getEnclaveUrl(); - } -} diff --git a/src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionGenerationException.java similarity index 95% rename from src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java rename to src/main/java/com/iexec/sms/tee/session/generic/TeeSessionGenerationException.java index 04fa9761..f02c257e 100644 --- a/src/main/java/com/iexec/sms/tee/session/TeeSessionGenerationException.java +++ b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionGenerationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.iexec.sms.tee.session; +package com.iexec.sms.tee.session.generic; import com.iexec.sms.api.TeeSessionGenerationError; diff --git a/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionHandler.java b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionHandler.java new file mode 100644 index 00000000..f69d83bb --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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 + * + * https://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. + */ + +package com.iexec.sms.tee.session.generic; + +public interface TeeSessionHandler { + + /** + * Build and post secret session on secret provisioning service. + * + * @param request tee session generation request + * @return String secret provisioning service url + * @throws TeeSessionGenerationException + */ + String buildAndPostSession(TeeSessionRequest request) throws TeeSessionGenerationException; + +} diff --git a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionRequest.java b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionRequest.java similarity index 92% rename from src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionRequest.java rename to src/main/java/com/iexec/sms/tee/session/generic/TeeSessionRequest.java index 16c5b86a..117d62ef 100644 --- a/src/main/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionRequest.java +++ b/src/main/java/com/iexec/sms/tee/session/generic/TeeSessionRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.iexec.sms.tee.session.palaemon; +package com.iexec.sms.tee.session.generic; import com.iexec.common.task.TaskDescription; import lombok.AllArgsConstructor; @@ -26,7 +26,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class PalaemonSessionRequest { +public class TeeSessionRequest { private String sessionId; private TaskDescription taskDescription; diff --git a/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerService.java b/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerService.java new file mode 100644 index 00000000..862899be --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerService.java @@ -0,0 +1,72 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import com.iexec.sms.tee.session.TeeSessionLogConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionHandler; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.gramine.sps.GramineSession; +import com.iexec.sms.tee.session.gramine.sps.SpsConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@ConditionalOnTeeFramework(frameworks = TeeFramework.GRAMINE) +public class GramineSessionHandlerService implements TeeSessionHandler { + private final GramineSessionMakerService sessionService; + private final SpsConfiguration spsConfiguration; + private final TeeSessionLogConfiguration teeSessionLogConfiguration; + + public GramineSessionHandlerService(GramineSessionMakerService sessionService, + SpsConfiguration spsConfiguration, + TeeSessionLogConfiguration teeSessionLogConfiguration) { + this.sessionService = sessionService; + this.spsConfiguration = spsConfiguration; + this.teeSessionLogConfiguration = teeSessionLogConfiguration; + } + + /** + * Build and post secret session on secret provisioning service. + * + * @param request tee session generation request + * @return String secret provisioning service url + * @throws TeeSessionGenerationException + */ + @Override + public String buildAndPostSession(TeeSessionRequest request) + throws TeeSessionGenerationException { + GramineSession session = sessionService.generateSession(request); + if (teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) { + log.info("Session content [taskId:{}]\n{}", + request.getTaskDescription().getChainTaskId(), session); + } + + try { + spsConfiguration.getInstance().postSession(session); + return spsConfiguration.getEnclaveHost(); + } catch (Exception e) { + throw new TeeSessionGenerationException( + TeeSessionGenerationError.SECURE_SESSION_STORAGE_CALL_FAILED, + "Failed to post session: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerService.java b/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerService.java new file mode 100644 index 00000000..86de236e --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerService.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import com.iexec.sms.tee.session.base.SecretEnclaveBase; +import com.iexec.sms.tee.session.base.SecretSessionBase; +import com.iexec.sms.tee.session.base.SecretSessionBaseService; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.gramine.sps.GramineEnclave; +import com.iexec.sms.tee.session.gramine.sps.GramineSession; +import com.iexec.sms.tee.session.gramine.sps.GramineSession.GramineSessionBuilder; +import lombok.NonNull; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@ConditionalOnTeeFramework(frameworks = TeeFramework.GRAMINE) +public class GramineSessionMakerService { + + private final SecretSessionBaseService secretSessionBaseService; + + public GramineSessionMakerService(SecretSessionBaseService secretSessionBaseService) { + this.secretSessionBaseService = secretSessionBaseService; + } + + /** + * Collect tokens required for different compute stages (pre, in, post) + * and build the JSON config of the TEE session. + * + * @param request session request details + * @return session config + */ + @NonNull + public GramineSession generateSession(TeeSessionRequest request) throws TeeSessionGenerationException { + SecretSessionBase baseSession = secretSessionBaseService.getSecretsTokens(request); + GramineSessionBuilder gramineSession = GramineSession.builder() + .session(request.getSessionId()); + GramineEnclave gramineAppEnclave = toGramineEnclave(baseSession.getAppCompute()); + GramineEnclave graminePostEnclave = toGramineEnclave(baseSession.getPostCompute()); + + // TODO: Validate command-line arguments from the host + // (https://github.com/gramineproject/gsc/issues/13) + gramineAppEnclave.setCommand(""); + graminePostEnclave.setCommand(""); + // TODO: Remove useless volumes when SPS is ready + gramineAppEnclave.setVolumes(List.of()); + graminePostEnclave.setVolumes(List.of()); + + return gramineSession.enclaves(List.of( + // No pre-compute for now + gramineAppEnclave, + graminePostEnclave)) + .build(); + } + + private GramineEnclave toGramineEnclave(SecretEnclaveBase enclaveBase) { + return GramineEnclave.builder() + .name(enclaveBase.getName()) + .mrenclave(enclaveBase.getMrenclave()) + .environment(enclaveBase.getEnvironment()) + .build(); + } + +} diff --git a/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineEnclave.java b/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineEnclave.java new file mode 100644 index 00000000..35f78060 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineEnclave.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine.sps; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GramineEnclave { + + @JsonProperty("name") + private String name; + @JsonProperty("mrenclave") + private String mrenclave; + @JsonProperty("command") + private String command; + @JsonProperty("environment") + private Map environment; + @JsonProperty("volumes") + private List volumes; + +} diff --git a/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineSession.java b/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineSession.java new file mode 100644 index 00000000..36c12edc --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/gramine/sps/GramineSession.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine.sps; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GramineSession { + + @JsonProperty("session") + private String session; + @JsonProperty("enclaves") + private List enclaves; + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + log.error("Failed to write SPS session as string [session:{}]", session, e); + return ""; + } + } + +} diff --git a/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java b/src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsApiClient.java similarity index 70% rename from src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java rename to src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsApiClient.java index 15135ea0..daff5d02 100644 --- a/src/test/java/com/iexec/sms/secret/web3/Web3SecretsServiceTests.java +++ b/src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsApiClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 IEXEC BLOCKCHAIN TECH + * Copyright 2022 IEXEC BLOCKCHAIN TECH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,13 @@ * limitations under the License. */ -package com.iexec.sms.secret.web3; +package com.iexec.sms.tee.session.gramine.sps; -class Web3SecretsServiceTests { +import feign.RequestLine; -} \ No newline at end of file +public interface SpsApiClient { + + @RequestLine("POST /api/session") + String postSession(GramineSession spsSession); + +} diff --git a/src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsConfiguration.java b/src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsConfiguration.java new file mode 100644 index 00000000..bb2e628a --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/gramine/sps/SpsConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine.sps; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.common.utils.FeignBuilder; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import feign.Logger.Level; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnTeeFramework(frameworks = TeeFramework.GRAMINE) +@Getter +public class SpsConfiguration { + @Value("${tee.secret-provisioner.web.hostname}") + private String webHost; + + @Value("${tee.secret-provisioner.web.port}") + private String webPort; + + @Value("${tee.gramine.sps.login}") + private String webLogin; + + @Value("${tee.gramine.sps.password}") + private String webPassword; + + @Value("${tee.secret-provisioner.enclave.hostname}") + private String enclaveHostName; + + @Value("${tee.secret-provisioner.enclave.port}") + private String enclavePort; + + private SpsApiClient spsApiClient; + + public String getWebUrl() { + return "http://" + webHost + ":" + webPort; + } + + public String getEnclaveHost() { + return enclaveHostName + ":" + enclavePort; + } + + public SpsApiClient getInstance() { + if (spsApiClient == null) { + spsApiClient = FeignBuilder.createBuilderWithBasicAuth(Level.FULL, + webLogin, webPassword) + .target(SpsApiClient.class, getWebUrl()); + } + return spsApiClient; + } + +} diff --git a/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerService.java b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerService.java new file mode 100644 index 00000000..a77e31c0 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerService.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.scone; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import com.iexec.sms.tee.session.TeeSessionLogConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionHandler; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.scone.cas.CasClient; +import com.iexec.sms.tee.session.scone.cas.CasConfiguration; +import com.iexec.sms.tee.session.scone.cas.SconeSession; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) +public class SconeSessionHandlerService implements TeeSessionHandler { + private final SconeSessionMakerService sessionService; + private final CasClient apiClient; + private final TeeSessionLogConfiguration teeSessionLogConfiguration; + private final CasConfiguration casConfiguration; + + public SconeSessionHandlerService(SconeSessionMakerService sessionService, + CasClient apiClient, + TeeSessionLogConfiguration teeSessionLogConfiguration, + CasConfiguration casConfiguration) { + this.sessionService = sessionService; + this.apiClient = apiClient; + this.teeSessionLogConfiguration = teeSessionLogConfiguration; + this.casConfiguration = casConfiguration; + } + + /** + * Build and post secret session on secret provisioning service. + * + * @param request tee session generation request + * @return String secret provisioning service url + * @throws TeeSessionGenerationException if call to CAS failed + */ + @Override + public String buildAndPostSession(TeeSessionRequest request) + throws TeeSessionGenerationException { + SconeSession session = sessionService.generateSession(request); + if (teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) { + log.info("Session content [taskId:{}]\n{}", + request.getTaskDescription().getChainTaskId(), session); + } + ResponseEntity postSession = apiClient.postSession(session.toString()); + + if (postSession == null) { + throw new TeeSessionGenerationException( + TeeSessionGenerationError.SECURE_SESSION_STORAGE_CALL_FAILED, + "Failed to post session, no return from CAS."); + } + + int httpCode = postSession.getStatusCodeValue(); + if (httpCode != 201) { + throw new TeeSessionGenerationException( + TeeSessionGenerationError.SECURE_SESSION_STORAGE_CALL_FAILED, + "Failed to post session: " + httpCode); + } + return casConfiguration.getEnclaveHost(); + } + +} diff --git a/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionMakerService.java b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionMakerService.java new file mode 100644 index 00000000..52b48cf1 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionMakerService.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.scone; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.config.TeeServicesProperties; +import com.iexec.sms.tee.ConditionalOnTeeFramework; +import com.iexec.sms.tee.session.base.SecretEnclaveBase; +import com.iexec.sms.tee.session.base.SecretSessionBase; +import com.iexec.sms.tee.session.base.SecretSessionBaseService; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.scone.cas.SconeEnclave; +import com.iexec.sms.tee.session.scone.cas.SconeSession; +import com.iexec.sms.tee.session.scone.cas.SconeSession.AccessPolicy; +import com.iexec.sms.tee.session.scone.cas.SconeSession.Image; +import com.iexec.sms.tee.session.scone.cas.SconeSession.Image.Volume; +import com.iexec.sms.tee.session.scone.cas.SconeSession.Security; +import com.iexec.sms.tee.session.scone.cas.SconeSession.Volumes; +import lombok.NonNull; +import org.springframework.stereotype.Service; + +import java.util.*; + +//TODO Rename and move +@Service +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) +public class SconeSessionMakerService { + + // Internal values required for setting up a palaemon session + // Generic + static final String TOLERATED_INSECURE_OPTIONS = "TOLERATED_INSECURE_OPTIONS"; + static final String IGNORED_SGX_ADVISORIES = "IGNORED_SGX_ADVISORIES"; + static final String APP_ARGS = "APP_ARGS"; + + // PreCompute + static final String PRE_COMPUTE_ENTRYPOINT = "PRE_COMPUTE_ENTRYPOINT"; + // PostCompute + static final String POST_COMPUTE_ENTRYPOINT = "POST_COMPUTE_ENTRYPOINT"; + + private final SecretSessionBaseService secretSessionBaseService; + private final TeeServicesProperties teeServicesConfig; + private final SconeSessionSecurityConfig attestationSecurityConfig; + + public SconeSessionMakerService( + SecretSessionBaseService secretSessionBaseService, + TeeServicesProperties teeServicesConfig, + SconeSessionSecurityConfig attestationSecurityConfig) { + this.secretSessionBaseService = secretSessionBaseService; + this.teeServicesConfig = teeServicesConfig; + this.attestationSecurityConfig = attestationSecurityConfig; + } + + /** + * Collect tokens required for different compute stages (pre, in, post) + * and build the yaml config of the TEE session. + *

    + * TODO: Read onchain available infos from enclave instead of copying + * public vars to palaemon.yml. It needs ssl call from enclave to eth + * node (only ethereum node address required inside palaemon.yml) + * + * @param request session request details + * @return session config in yaml string format + */ + @NonNull + public SconeSession generateSession(TeeSessionRequest request) + throws TeeSessionGenerationException { + List policy = Arrays.asList("CREATOR"); + Volume iexecInVolume = new Volume("iexec_in", "/iexec_in"); + Volume iexecOutVolume = new Volume("iexec_out", "/iexec_out"); + Volume postComputeTmpVolume = new Volume("post-compute-tmp", + "/post-compute-tmp"); + List services = new ArrayList<>(); + List images = new ArrayList<>(); + + SecretSessionBase baseSession = secretSessionBaseService + .getSecretsTokens(request); + + // pre (optional) + if (baseSession.getPreCompute() != null) { + SconeEnclave sconePreEnclave = toSconeEnclave( + baseSession.getPreCompute()); + sconePreEnclave + .setCommand(teeServicesConfig.getPreComputeProperties().getEntrypoint()); + addJavaEnvVars(sconePreEnclave); + services.add(sconePreEnclave); + images.add(new SconeSession.Image(sconePreEnclave.getImageName(), + Arrays.asList(iexecInVolume))); + } + // app + SconeEnclave sconeAppEnclave = toSconeEnclave( + baseSession.getAppCompute()); + sconeAppEnclave + .setCommand(request.getTaskDescription().getAppCommand()); + services.add(sconeAppEnclave); + images.add(new SconeSession.Image(sconeAppEnclave.getImageName(), + Arrays.asList(iexecInVolume, iexecOutVolume))); + // post + SconeEnclave sconePostEnclave = toSconeEnclave( + baseSession.getPostCompute()); + sconePostEnclave + .setCommand(teeServicesConfig.getPostComputeProperties().getEntrypoint()); + addJavaEnvVars(sconePostEnclave); + services.add(sconePostEnclave); + images.add(new SconeSession.Image(sconePostEnclave.getImageName(), + Arrays.asList(iexecOutVolume, + postComputeTmpVolume))); + + return SconeSession.builder() + .name(request.getSessionId()) + .version("0.3") + .accessPolicy(new AccessPolicy(policy, policy)) + .services(services) + .images(images) + .volumes(Arrays.asList(new Volumes(iexecInVolume.getName()), + new Volumes(iexecOutVolume.getName()), + new Volumes(postComputeTmpVolume.getName()))) + .security(new Security( + attestationSecurityConfig.getToleratedInsecureOptions(), + attestationSecurityConfig.getIgnoredSgxAdvisories())) + .build(); + } + + private void addJavaEnvVars(SconeEnclave sconeEnclave) { + Map additionalJavaEnv = Map.of("LD_LIBRARY_PATH", + "/usr/lib/jvm/java-11-openjdk/lib/server:/usr/lib/jvm/java-11-openjdk/lib:/usr/lib/jvm/java-11-openjdk/../lib", + "JAVA_TOOL_OPTIONS", "-Xmx256m"); + HashMap newEnvironment = new HashMap<>(); + newEnvironment.putAll(sconeEnclave.getEnvironment()); + newEnvironment.putAll(additionalJavaEnv); + sconeEnclave.setEnvironment(newEnvironment); + } + + private SconeEnclave toSconeEnclave(SecretEnclaveBase enclaveBase) { + return SconeEnclave.builder() + .name(enclaveBase.getName()) + .imageName(enclaveBase.getName() + "-image") + .mrenclaves(Arrays.asList(enclaveBase.getMrenclave())) + .pwd("/") + // TODO .command(command) + .environment(enclaveBase.getEnvironment()) + .build(); + } + +} diff --git a/src/main/java/com/iexec/sms/tee/session/attestation/AttestationSecurityConfig.java b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionSecurityConfig.java similarity index 71% rename from src/main/java/com/iexec/sms/tee/session/attestation/AttestationSecurityConfig.java rename to src/main/java/com/iexec/sms/tee/session/scone/SconeSessionSecurityConfig.java index ca750cb1..1124f157 100644 --- a/src/main/java/com/iexec/sms/tee/session/attestation/AttestationSecurityConfig.java +++ b/src/main/java/com/iexec/sms/tee/session/scone/SconeSessionSecurityConfig.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.iexec.sms.tee.session.attestation; +package com.iexec.sms.tee.session.scone; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.ConditionalOnTeeFramework; import lombok.Getter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; @@ -23,13 +25,14 @@ import java.util.List; @Configuration -public class AttestationSecurityConfig { +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) +public class SconeSessionSecurityConfig { - @Value("${scone.attestation.tolerated-insecure-options}") + @Value("${tee.scone.attestation.tolerated-insecure-options}") @Getter private List toleratedInsecureOptions; - @Value("${scone.attestation.ignored-sgx-advisories}") + @Value("${tee.scone.attestation.ignored-sgx-advisories}") @Getter private List ignoredSgxAdvisories; } diff --git a/src/main/java/com/iexec/sms/tee/session/cas/CasClient.java b/src/main/java/com/iexec/sms/tee/session/scone/cas/CasClient.java similarity index 77% rename from src/main/java/com/iexec/sms/tee/session/cas/CasClient.java rename to src/main/java/com/iexec/sms/tee/session/scone/cas/CasClient.java index acffcda3..0ed6f964 100644 --- a/src/main/java/com/iexec/sms/tee/session/cas/CasClient.java +++ b/src/main/java/com/iexec/sms/tee/session/scone/cas/CasClient.java @@ -14,31 +14,36 @@ * limitations under the License. */ -package com.iexec.sms.tee.session.cas; +package com.iexec.sms.tee.session.scone.cas; +import com.iexec.common.tee.TeeFramework; import com.iexec.sms.ssl.TwoWaySslClient; +import com.iexec.sms.tee.ConditionalOnTeeFramework; import org.springframework.http.HttpEntity; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.nio.charset.StandardCharsets; + @Service +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) public class CasClient { private final CasConfiguration casConfiguration; private final TwoWaySslClient twoWaySslClient; public CasClient(CasConfiguration teeCasConfiguration, - TwoWaySslClient twoWaySslClient) { + TwoWaySslClient twoWaySslClient) { this.casConfiguration = teeCasConfiguration; this.twoWaySslClient = twoWaySslClient; } /* * POST /session of CAS requires 2-way SSL authentication - * */ - public ResponseEntity generateSecureSession(byte[] palaemonFile) { + */ + public ResponseEntity postSession(String palaemonFile) { String url = casConfiguration.getUrl() + "/session"; - HttpEntity request = new HttpEntity<>(palaemonFile); + HttpEntity request = new HttpEntity<>(palaemonFile.getBytes(StandardCharsets.UTF_8)); return twoWaySslClient .getRestTemplate() .postForEntity(url, request, String.class); diff --git a/src/main/java/com/iexec/sms/tee/session/cas/CasConfiguration.java b/src/main/java/com/iexec/sms/tee/session/scone/cas/CasConfiguration.java similarity index 78% rename from src/main/java/com/iexec/sms/tee/session/cas/CasConfiguration.java rename to src/main/java/com/iexec/sms/tee/session/scone/cas/CasConfiguration.java index f471b6ef..eeb96ac0 100644 --- a/src/main/java/com/iexec/sms/tee/session/cas/CasConfiguration.java +++ b/src/main/java/com/iexec/sms/tee/session/scone/cas/CasConfiguration.java @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.iexec.sms.tee.session.cas; +package com.iexec.sms.tee.session.scone.cas; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.ConditionalOnTeeFramework; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -29,33 +31,34 @@ * When a service wants to access those secrets, it sends a quote with its MREnclave. * The CAS attests the quote through Intel Attestation Service and sends the secrets * if the MREnclave is as expected. - * + *

    * MREnclave: an enclave identifier, created by hashing all its * code. It guarantees that a code behaves exactly as expected. */ @Component +@ConditionalOnTeeFramework(frameworks = TeeFramework.SCONE) @Getter @NoArgsConstructor @AllArgsConstructor public class CasConfiguration { - @Value("${scone.cas.host}") + @Value("${tee.secret-provisioner.web.hostname}") private String host; - @Value("${scone.cas.port}") + @Value("${tee.secret-provisioner.web.port}") private String port; - @Value("${scone.cas.public-host}") + @Value("${tee.secret-provisioner.enclave.hostname}") private String publicHost; - @Value("${scone.cas.enclave-port}") + @Value("${tee.secret-provisioner.enclave.port}") private String enclavePort; public String getUrl() { return "https://" + host + ":" + port; } - public String getEnclaveUrl() { + public String getEnclaveHost() { return publicHost + ":" + enclavePort; } } diff --git a/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeEnclave.java b/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeEnclave.java new file mode 100644 index 00000000..f41709c0 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeEnclave.java @@ -0,0 +1,59 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.scone.cas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; + +@Slf4j +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class SconeEnclave { + + @JsonProperty("name") + private String name; + @JsonProperty("image_name") + private String imageName; + @JsonProperty("mrenclaves") + private List mrenclaves; + @JsonProperty("pwd") + private String pwd; + @JsonProperty("command") + private String command; + @JsonProperty("environment") + private Map environment; + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + log.error("Failed to write CAS session as string [session:{}]", name, e); + return ""; + } + } + +} diff --git a/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeSession.java b/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeSession.java new file mode 100644 index 00000000..7f837e04 --- /dev/null +++ b/src/main/java/com/iexec/sms/tee/session/scone/cas/SconeSession.java @@ -0,0 +1,112 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.scone.cas; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +@Builder +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class SconeSession { + + @JsonProperty("name") + private String name; + @JsonProperty("version") + private String version; + @JsonProperty("access_policy") + private AccessPolicy accessPolicy; + @JsonProperty("services") + private List services; + @JsonProperty("images") + private List images; + @JsonProperty("volumes") + private List volumes; + @JsonProperty("security") + private Security security; + + @AllArgsConstructor + @Getter + public static class AccessPolicy { + @JsonProperty("read") + private List read; + @JsonProperty("update") + private List update; + } + + @AllArgsConstructor + public static class Image { + @JsonProperty("name") + private String name; + @JsonProperty("volumes") + private List volumes; + + @Getter + @AllArgsConstructor + public static class Volume { + @JsonProperty("name") + private String name; + @JsonProperty("path") + private String path; + } + } + + @AllArgsConstructor + public static class Volumes { + @JsonProperty("name") + private String name; + } + + @AllArgsConstructor + @Getter + public static class Security { + @JsonProperty("attestation") + private Attestation attestation; + + public Security(List tolerate, List ignoreAdvisories) { + this.attestation = new Attestation(tolerate, ignoreAdvisories); + } + + @AllArgsConstructor + @Getter + public class Attestation { + @JsonProperty("tolerate") + private List tolerate; + @JsonProperty("ignore_advisories") + private List ignoreAdvisories; + } + } + + @Override + public String toString() { + try { + return new YAMLMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + log.error("Failed to write SPS session as string [session:{}]", name, e); + return ""; + } + } + +} diff --git a/src/main/java/com/iexec/sms/tee/workflow/TeeWorkflowConfiguration.java b/src/main/java/com/iexec/sms/tee/workflow/TeeWorkflowConfiguration.java deleted file mode 100644 index 9c5859a7..00000000 --- a/src/main/java/com/iexec/sms/tee/workflow/TeeWorkflowConfiguration.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.tee.workflow; - -import com.iexec.common.tee.TeeWorkflowSharedConfiguration; -import lombok.AccessLevel; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.unit.DataSize; - -import javax.annotation.PostConstruct; -import javax.validation.ConstraintViolationException; -import javax.validation.Validator; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Positive; - -@Configuration -@Getter -public class TeeWorkflowConfiguration { - - @Value("${tee.workflow.las-image}") - @NotBlank(message = "las image must be provided") - String lasImage; - - @Value("${tee.workflow.pre-compute.image}") - @NotBlank(message = "pre-compute image must be provided") - String preComputeImage; - - @Value("${tee.workflow.pre-compute.fingerprint}") - @NotBlank(message = "pre-compute fingerprint must be provided") - String preComputeFingerprint; - - @Value("${tee.workflow.pre-compute.entrypoint}") - @NotBlank(message = "pre-compute entrypoint must be provided") - String preComputeEntrypoint; - - @Value("${tee.workflow.pre-compute.heap-size-gb}") - @Positive(message = "pre-compute heap size must be provided") - int preComputeHeapSizeGb; - - @Value("${tee.workflow.post-compute.image}") - @NotBlank(message = "post-compute image must be provided") - String postComputeImage; - - @Value("${tee.workflow.post-compute.fingerprint}") - @NotBlank(message = "post-compute fingerprint must be provided") - String postComputeFingerprint; - - @Value("${tee.workflow.post-compute.entrypoint}") - @NotBlank(message = "post-compute entrypoint must be provided") - String postComputeEntrypoint; - - @Value("${tee.workflow.post-compute.heap-size-gb}") - @Positive(message = "post-compute heap size must be provided") - int postComputeHeapSizeGb; - - @Getter(AccessLevel.NONE) // no getter - private Validator validator; - - public TeeWorkflowConfiguration(Validator validator) { - this.validator = validator; - } - - @PostConstruct - private void validate() { - if (!validator.validate(this).isEmpty()) { - throw new ConstraintViolationException(validator.validate(this)); - } - } - - public TeeWorkflowSharedConfiguration getSharedConfiguration() { - return TeeWorkflowSharedConfiguration.builder() - .lasImage(lasImage) - .preComputeImage(preComputeImage) - .preComputeEntrypoint(preComputeEntrypoint) - .preComputeHeapSize(DataSize - .ofGigabytes(preComputeHeapSizeGb) - .toBytes()) - .postComputeImage(postComputeImage) - .postComputeEntrypoint(postComputeEntrypoint) - .postComputeHeapSize(DataSize - .ofGigabytes(postComputeHeapSizeGb) - .toBytes()) - .build(); - } -} diff --git a/src/main/java/com/iexec/sms/untee/UnTeeController.java b/src/main/java/com/iexec/sms/untee/UnTeeController.java deleted file mode 100644 index 3f5161f9..00000000 --- a/src/main/java/com/iexec/sms/untee/UnTeeController.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.untee; - - -import java.util.Optional; - -import com.iexec.common.chain.WorkerpoolAuthorization; -import com.iexec.common.sms.secret.SmsSecretResponse; -import com.iexec.common.sms.secret.SmsSecretResponseData; -import com.iexec.common.sms.secret.TaskSecrets; -import com.iexec.sms.authorization.AuthorizationService; -import com.iexec.sms.untee.secret.UnTeeSecretService; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class UnTeeController { - - private UnTeeSecretService unTeeSecretService; - private AuthorizationService authorizationService; - - public UnTeeController( - AuthorizationService authorizationService, - UnTeeSecretService unTeeSecretService) { - this.authorizationService = authorizationService; - this.unTeeSecretService = unTeeSecretService; - } - - /* - * Retrieve secrets when non-tee execution : We shouldn't do this.. - * */ - @PostMapping("/untee/secrets") - public ResponseEntity getUnTeeSecrets(@RequestHeader("Authorization") String authorization, - @RequestBody WorkerpoolAuthorization workerpoolAuthorization) { - String workerAddress = workerpoolAuthorization.getWorkerWallet(); - String challenge = authorizationService.getChallengeForWorker(workerpoolAuthorization); - if (!authorizationService.isSignedByHimself(challenge, authorization, workerAddress)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - if (!authorizationService.isAuthorizedOnExecution(workerpoolAuthorization, false)) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); - } - - Optional unTeeTaskSecrets = unTeeSecretService.getUnTeeTaskSecrets(workerpoolAuthorization.getChainTaskId()); - if (unTeeTaskSecrets.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - - SmsSecretResponseData smsSecretResponseData = SmsSecretResponseData.builder() - .secrets(unTeeTaskSecrets.get()) - .build(); - - SmsSecretResponse smsSecretResponse = SmsSecretResponse.builder() - .data(smsSecretResponseData) - .build(); - - return Optional.of(smsSecretResponse).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); - } - - -} - diff --git a/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java b/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java deleted file mode 100644 index 7e97b60c..00000000 --- a/src/main/java/com/iexec/sms/untee/secret/UnTeeSecretService.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.untee.secret; - -import com.iexec.common.chain.ChainDeal; -import com.iexec.common.chain.ChainTask; -import com.iexec.common.sms.secret.SmsSecret; -import com.iexec.common.sms.secret.TaskSecrets; -import com.iexec.sms.blockchain.IexecHubService; -import com.iexec.sms.secret.Secret; -import com.iexec.sms.secret.web2.Web2SecretsService; -import com.iexec.sms.secret.web3.Web3Secret; -import com.iexec.sms.secret.web3.Web3SecretService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -import static com.iexec.sms.secret.ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY; - -@Service -@Slf4j -public class UnTeeSecretService { - - private final IexecHubService iexecHubService; - private final Web3SecretService web3SecretService; - private final Web2SecretsService web2SecretsService; - - public UnTeeSecretService(IexecHubService iexecHubService, - Web3SecretService web3SecretService, - Web2SecretsService web2SecretsService - ) { - this.iexecHubService = iexecHubService; - this.web3SecretService = web3SecretService; - this.web2SecretsService = web2SecretsService; - } - - /* - * Untested yet - */ - public Optional getUnTeeTaskSecrets(String chainTaskId) { - TaskSecrets.TaskSecretsBuilder taskSecretsBuilder = TaskSecrets.builder(); - - // TODO use taskDescription instead of chainDeal - Optional oChainTask = iexecHubService.getChainTask(chainTaskId); - if (oChainTask.isEmpty()) { - log.error("getUnTeeTaskSecrets failed (getChainTask failed) [chainTaskId:{}]", chainTaskId); - return Optional.empty(); - } - ChainTask chainTask = oChainTask.get(); - Optional oChainDeal = iexecHubService.getChainDeal(chainTask.getDealid()); - if (oChainDeal.isEmpty()) { - log.error("getUnTeeTaskSecrets failed (getChainDeal failed) [chainTaskId:{}]", chainTaskId); - return Optional.empty(); - } - ChainDeal chainDeal = oChainDeal.get(); - String chainDatasetId = chainDeal.getChainDataset().getChainDatasetId(); - - Optional datasetSecret = web3SecretService.getSecret(chainDatasetId, true); - if (datasetSecret.isEmpty()) { - log.error("getUnTeeTaskSecrets failed (datasetSecret failed) [chainTaskId:{}]", chainTaskId); - return Optional.empty(); - } - taskSecretsBuilder.datasetSecret(SmsSecret.builder() - .address(datasetSecret.get().getAddress()) - .secret(datasetSecret.get().getValue()) - .build()); - - Optional beneficiarySecret = web2SecretsService.getSecret(chainDeal.getBeneficiary(), - IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, true); - if (beneficiarySecret.isEmpty()) { - log.error("getUnTeeTaskSecrets failed (beneficiarySecret empty) [chainTaskId:{}]", chainTaskId); - return Optional.empty(); - } - taskSecretsBuilder.beneficiarySecret(SmsSecret.builder() - .address(beneficiarySecret.get().getAddress()) - .secret(beneficiarySecret.get().getValue()) - .build()); - - return Optional.of(taskSecretsBuilder.build()); - } - -} diff --git a/src/main/resources/Dockerfile b/src/main/resources/Dockerfile deleted file mode 100644 index 7327ed5c..00000000 --- a/src/main/resources/Dockerfile +++ /dev/null @@ -1,55 +0,0 @@ -FROM nexus.iex.ec/sconecuratedimages-apps-java:jdk-alpine-scone3.0 - -ARG jar - -RUN test -n "$jar" - -COPY $jar /app/iexec-sms.jar -COPY build/resources/main/palaemonTemplate.vm /app/palaemonTemplate.vm -COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 - -# #### Docker ENV vars should be placed in palaemon conf for Scone ### - -RUN SCONE_MODE=sim SCONE_HEAP=128M && \ - # This path must match the one in palaemon session. - FSPF_PB_FILE=/fspf.pb && \ - # This path can be changed safely. - FSPF_PB_FILE_KEYTAG=/fspf.keytag && \ - # This path can be change safely. - FINGERPRINT_FILE=/fingerprint.txt && \ - # This value must match the heap size value - # provided at runtime. - RUNTIME_HEAP_SIZE=3G && \ - # Save the current file system state in fspf.pb file. - scone fspf create ${FSPF_PB_FILE} && \ - scone fspf addr ${FSPF_PB_FILE} / --not-protected --kernel / && \ - scone fspf addr ${FSPF_PB_FILE} /usr --authenticated --kernel /usr && \ - scone fspf addf ${FSPF_PB_FILE} /usr /usr && \ - scone fspf addr ${FSPF_PB_FILE} /bin --authenticated --kernel /bin && \ - scone fspf addf ${FSPF_PB_FILE} /bin /bin && \ - scone fspf addr ${FSPF_PB_FILE} /lib --authenticated --kernel /lib && \ - scone fspf addf ${FSPF_PB_FILE} /lib /lib && \ - scone fspf addr ${FSPF_PB_FILE} /etc/ssl --authenticated --kernel /etc/ssl && \ - scone fspf addf ${FSPF_PB_FILE} /etc/ssl /etc/ssl && \ - scone fspf addr ${FSPF_PB_FILE} /sbin --authenticated --kernel /sbin && \ - scone fspf addf ${FSPF_PB_FILE} /sbin /sbin && \ - scone fspf addr ${FSPF_PB_FILE} /app --authenticated --kernel /app && \ - scone fspf addf ${FSPF_PB_FILE} /app /app && \ - # Encrypt fspf.pb file with a randomly generated key - scone fspf encrypt ${FSPF_PB_FILE} > ${FSPF_PB_FILE_KEYTAG} && \ - # Get the runtime mrenclave - MRENCLAVE="$(SCONE_HASH=1 SCONE_HEAP=${RUNTIME_HEAP_SIZE} java)" && \ - # Get fspf.pb file tag - FSPF_TAG=$(awk '{print $9}' ${FSPF_PB_FILE_KEYTAG}) && \ - # Get fspf.pb file key - FSPF_KEY=$(awk '{print $11}' ${FSPF_PB_FILE_KEYTAG}) && \ - # The complete fingerprint is composed of 3 parts - FINGERPRINT="${FSPF_KEY}|${FSPF_TAG}|${MRENCLAVE}" && \ - echo ${FINGERPRINT} > ${FINGERPRINT_FILE} && \ - echo "${MRENCLAVE}" && \ - echo "${FSPF_KEY}" && \ - echo "${FSPF_TAG}" && \ - echo "${FINGERPRINT}" - -# /!\ This should match the "command" entry in the palaemon config -ENTRYPOINT [ "/bin/sh", "-c", "java -jar /app/iexec-sms.jar" ] diff --git a/src/main/resources/Dockerfile.untrusted b/src/main/resources/Dockerfile.untrusted deleted file mode 100644 index 1616e59a..00000000 --- a/src/main/resources/Dockerfile.untrusted +++ /dev/null @@ -1,22 +0,0 @@ -FROM openjdk:11.0.15-jre-slim - -ARG jar - -RUN test -n "$jar" - -RUN apt-get update \ - && apt-get install -y curl \ - && rm -rf /var/lib/apt/lists/* - -COPY $jar /app/iexec-sms.jar -COPY build/resources/main/palaemonTemplate.vm /app/palaemonTemplate.vm -COPY src/main/resources/ssl-keystore-dev.p12 /app/ssl-keystore-dev.p12 - -ENV IEXEC_PALAEMON_TEMPLATE=/app/palaemonTemplate.vm -ENV IEXEC_SMS_SSL_KEYSTORE=/app/ssl-keystore-dev.p12 -ENV IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH=/scone/iexec-sms-aes.key -ENV IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS=http://chain:8545 -ENV IEXEC_SCONE_CAS_HOST=iexec-cas -ENV IEXEC_SMS_H2_URL=jdbc:h2:file:/scone/sms-h2 - -ENTRYPOINT [ "/bin/sh", "-c", "java -jar /app/iexec-sms.jar" ] diff --git a/src/main/resources/application-gramine.yml b/src/main/resources/application-gramine.yml new file mode 100644 index 00000000..e505ae7c --- /dev/null +++ b/src/main/resources/application-gramine.yml @@ -0,0 +1,25 @@ +tee: + secret-provisioner: + web: + hostname: ${IEXEC_SECRET_PROVISIONER_WEB_HOSTNAME:localhost} + port: ${IEXEC_SECRET_PROVISIONER_WEB_PORT:8080} + enclave: + hostname: ${IEXEC_SECRET_PROVISIONER_ENCLAVE_HOSTNAME:localhost} + port: ${IEXEC_SECRET_PROVISIONER_ENCLAVE_PORT:4433} + + worker: + pre-compute: + image: ${IEXEC_TEE_WORKER_PRE_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-pre-compute:x.y.z-production + fingerprint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_FINGERPRINT:} + heap-size-gb: ${IEXEC_TEE_WORKER_PRE_COMPUTE_HEAP_SIZE_GB:3} + entrypoint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_ENTRYPOINT:/bin/bash /apploader.sh} + post-compute: + image: ${IEXEC_TEE_WORKER_POST_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-post-compute:x.y.z-production + fingerprint: ${IEXEC_TEE_WORKER_POST_COMPUTE_FINGERPRINT:} + heap-size-gb: ${IEXEC_TEE_WORKER_POST_COMPUTE_HEAP_SIZE_GB:3} + entrypoint: ${IEXEC_TEE_WORKER_POST_COMPUTE_ENTRYPOINT:/bin/bash /apploader.sh} + + gramine: + sps: + login: ${IEXEC_GRAMINE_SPS_WEB_LOGIN:admin} + password: ${IEXEC_GRAMINE_SPS_WEB_PASSWORD:admin} diff --git a/src/main/resources/application-scone.yml b/src/main/resources/application-scone.yml new file mode 100644 index 00000000..eec7fc96 --- /dev/null +++ b/src/main/resources/application-scone.yml @@ -0,0 +1,33 @@ +tee: + secret-provisioner: + web: + hostname: ${IEXEC_SECRET_PROVISIONER_WEB_HOSTNAME:localhost} + port: ${IEXEC_SECRET_PROVISIONER_WEB_PORT:8081} + enclave: + hostname: ${IEXEC_SECRET_PROVISIONER_ENCLAVE_HOSTNAME:localhost} + port: ${IEXEC_SECRET_PROVISIONER_ENCLAVE_PORT:18765} + + worker: + pre-compute: + image: ${IEXEC_TEE_WORKER_PRE_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-pre-compute:x.y.z-production + fingerprint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_FINGERPRINT:} + heap-size-gb: ${IEXEC_TEE_WORKER_PRE_COMPUTE_HEAP_SIZE_GB:3} + entrypoint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_ENTRYPOINT:java -jar /app/app.jar} + post-compute: + image: ${IEXEC_TEE_WORKER_POST_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-post-compute:x.y.z-production + fingerprint: ${IEXEC_TEE_WORKER_POST_COMPUTE_FINGERPRINT:} + heap-size-gb: ${IEXEC_TEE_WORKER_POST_COMPUTE_HEAP_SIZE_GB:3} + entrypoint: ${IEXEC_TEE_WORKER_POST_COMPUTE_ENTRYPOINT:java -jar /app/app.jar} + + ssl: + key-store: ${IEXEC_SMS_SSL_KEYSTORE:/app/ssl-keystore-dev.p12} #iexec-core dev certificate for dev + key-store-password: ${IEXEC_SMS_SSL_KEYSTORE_PASSWORD:whatever} + key-store-type: ${IEXEC_SMS_SSL_KEYSTORE_TYPE:PKCS12} + key-alias: ${IEXEC_SMS_SSL_KEYSTORE_ALIAS:iexec-core} + client-auth: need + + scone: + las-image: ${IEXEC_SMS_IMAGE_LAS_IMAGE:} # e.g.: registry.scontain.com:5050/scone-production/iexec-las:x.y.z + attestation: + tolerated-insecure-options: ${IEXEC_SCONE_TOLERATED_INSECURE_OPTIONS:} # e.g.: hyperthreading,software-hardening-needed,insecure-igpu,outdated-tcb,debug-mode + ignored-sgx-advisories: ${IEXEC_IGNORED_SGX_ADVISORIES:} # e.g.: INTEL-SA-00220,INTEL-SA-00270 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1d4cb585..63c00e9c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,14 +1,5 @@ server: - port: ${IEXEC_SMS_PORT:15443} - http: - enabled: ${IEXEC_SMS_HTTP_ENABLED:true} - port: ${IEXEC_SMS_HTTP_PORT:13300} - ssl: - key-store: ${IEXEC_SMS_SSL_KEYSTORE:./src/main/resources/ssl-keystore-dev.p12} #iexec-core dev certificate for dev - key-store-password: ${IEXEC_SMS_SSL_KEYSTORE_PASSWORD:whatever} - key-store-type: ${IEXEC_SMS_SSL_KEYSTORE_TYPE:PKCS12} - key-alias: ${IEXEC_SMS_SSL_KEYSTORE_ALIAS:iexec-core} - client-auth: need + port: ${IEXEC_SMS_PORT:13300} # Not sure it's a good idea but here is a link for an embedded mongodb # https://www.baeldung.com/spring-boot-embedded-mongodb @@ -22,8 +13,10 @@ server: # Embedded H2 inside JVM spring: + profiles: + active: ${IEXEC_SMS_TEE_RUNTIME_FRAMEWORK:} # gramine/scone datasource: - url: ${IEXEC_SMS_H2_URL:jdbc:h2:file:/tmp/h2/sms-h2} # will get or create /tmp/h2/sms-h2.mv.db + url: ${IEXEC_SMS_H2_URL:jdbc:h2:file:/data/sms-h2} # will get or create /data/sms-h2.mv.db driver-class-name: org.h2.Driver username: sa password: @@ -39,41 +32,16 @@ spring: encryption: # Will get previous key or else create one on this path # this file shouldn't be clearly readable outside the enclave (but encrypted content could be copied outside) - aesKeyPath: ${IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH:./src/main/resources/iexec-sms-aes.key} # /scone/iexec-sms-aes.key + aesKeyPath: ${IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH:/data/iexec-sms-aes.key} blockchain: id: ${IEXEC_CHAIN_ID:17} node-address: ${IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS:http://localhost:8545} hub-address: ${IEXEC_HUB_ADDRESS:0xBF6B2B07e47326B7c8bfCb4A5460bef9f0Fd2002} - gas-price-multiplier: ${IEXEC_GAS_PRICE_MULTIPLIER:1.0} # txs will be sent with networkGasPrice*gasPriceMultiplier, 4.0 means super fast + gas-price-multiplier: ${IEXEC_GAS_PRICE_MULTIPLIER:1.0} # txs will be sent with networkGasPrice*gasPriceMultiplier, 4.0 means superfast gas-price-cap: ${IEXEC_GAS_PRICE_CAP:22000000000} #in Wei, will be used for txs if networkGasPrice*gasPriceMultiplier > gasPriceCap is-sidechain: ${IEXEC_IS_SIDECHAIN:false} -scone: - cas: - host: ${IEXEC_SCONE_CAS_HOST:localhost} - port: ${IEXEC_SCONE_CAS_PORT:8081} - public-host: ${IEXEC_SCONE_CAS_PUBLIC_HOST:localhost} - enclave-port: ${IEXEC_SCONE_CAS_ENCLAVE_PORT:18765} - palaemon: ${IEXEC_PALAEMON_TEMPLATE:./src/main/resources/palaemonTemplate.vm} - attestation: - tolerated-insecure-options: ${IEXEC_SCONE_TOLERATED_INSECURE_OPTIONS:} # e.g.: hyperthreading,software-hardening-needed,insecure-igpu,outdated-tcb,debug-mode - ignored-sgx-advisories: ${IEXEC_IGNORED_SGX_ADVISORIES:} # e.g.: INTEL-SA-00220,INTEL-SA-00270 - -# TODO /!\ remove the option of env variable for releases. -tee.workflow: - las-image: ${IEXEC_SMS_IMAGE_LAS_IMAGE:} # e.g.: registry.scontain.com:5050/scone-production/iexec-las:x.y.z - pre-compute: - image: ${IEXEC_TEE_WORKER_PRE_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-pre-compute:x.y.z-production - fingerprint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_FINGERPRINT:} - heap-size-gb: ${IEXEC_TEE_WORKER_PRE_COMPUTE_HEAP_SIZE_GB:4} - entrypoint: ${IEXEC_TEE_WORKER_PRE_COMPUTE_ENTRYPOINT:java -jar /app/app.jar} - post-compute: - image: ${IEXEC_TEE_WORKER_POST_COMPUTE_IMAGE:} # e.g.: docker.io/iexechub/tee-worker-post-compute:x.y.z-production - fingerprint: ${IEXEC_TEE_WORKER_POST_COMPUTE_FINGERPRINT:} - heap-size-gb: ${IEXEC_TEE_WORKER_POST_COMPUTE_HEAP_SIZE_GB:4} - entrypoint: ${IEXEC_TEE_WORKER_POST_COMPUTE_ENTRYPOINT:java -jar /app/app.jar} - logging: tee.display-debug-session: ${IEXEC_SMS_DISPLAY_DEBUG_SESSION:false} # level: @@ -82,4 +50,4 @@ logging: springdoc: packagesToScan: com.iexec.sms - pathsToMatch: /** \ No newline at end of file + pathsToMatch: /** diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 00000000..2bb249c3 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,7 @@ +${Ansi.YELLOW} _ _____ ______ +${Ansi.YELLOW} __/\__ (_) ____|_ _____ ___ \ \ \ \ +${Ansi.YELLOW} \ / | | _| \ \/ / _ \/ __| \ \ \ \ +${Ansi.YELLOW} /_ _\ | | |___ > < __/ (__ / / / / +${Ansi.YELLOW} \/ |_|_____/_/\_\___|\___| /_/_/_/ +${Ansi.YELLOW} ========= +${Ansi.YELLOW} :: ${application.title}${application.formatted-version} built with Spring Boot${spring-boot.formatted-version} :: ${Ansi.DEFAULT} \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..411ed485 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/palaemonTemplate.vm b/src/main/resources/palaemonTemplate.vm deleted file mode 100644 index 645c75fe..00000000 --- a/src/main/resources/palaemonTemplate.vm +++ /dev/null @@ -1,143 +0,0 @@ -#* - It is safer to keep all values single-quoted to escape yaml - special characters (in dataset names for example). We also - ran into some parsing issues on the CAS's side when integers - were not quoted ("invalid type: integer `0`, expected a string"). - - Null values should be replaced by an empty string in the code - otherwise we end up with 'null' instead of ''. -*# - -name: '$SESSION_ID' -version: '0.3' - -access_policy: - read: - - CREATOR - update: - - CREATOR - -services: - - #* - Pre-compute enclave. - Only when dataset is requested. - *# - #if ($IS_PRE_COMPUTE_REQUIRED) - - name: 'pre-compute' - image_name: 'pre-compute-image' - mrenclaves: ['$PRE_COMPUTE_MRENCLAVE'] - pwd: '/' - command: '$PRE_COMPUTE_ENTRYPOINT' - environment: - LD_LIBRARY_PATH: '/usr/lib/jvm/java-11-openjdk/lib/server:/usr/lib/jvm/java-11-openjdk/lib:/usr/lib/jvm/java-11-openjdk/../lib' - JAVA_TOOL_OPTIONS: '-Xmx256m' - IEXEC_TASK_ID: '$env.get("IEXEC_TASK_ID")' - IEXEC_PRE_COMPUTE_OUT: '/iexec_in' - #* dataset *# - IS_DATASET_REQUIRED: '$IS_DATASET_REQUIRED' - IEXEC_DATASET_KEY: '$IEXEC_DATASET_KEY' - IEXEC_DATASET_URL: '$env.get("IEXEC_DATASET_URL")' - IEXEC_DATASET_FILENAME: '$env.get("IEXEC_DATASET_FILENAME")' - IEXEC_DATASET_CHECKSUM: '$env.get("IEXEC_DATASET_CHECKSUM")' - #* input files *# - IEXEC_INPUT_FILES_FOLDER: '$env.get("IEXEC_INPUT_FILES_FOLDER")' - IEXEC_INPUT_FILES_NUMBER: '$env.get("IEXEC_INPUT_FILES_NUMBER")' - #foreach($key in $INPUT_FILE_URLS.keySet()) - $key: '$INPUT_FILE_URLS.get($key)' - #end - #end - - #* - Application enclave - *# - - name: 'app' - image_name: 'app-image' - mrenclaves: ['$APP_MRENCLAVE'] - pwd: '/' - command: '$APP_ARGS' - environment: - IEXEC_TASK_ID: '$env.get("IEXEC_TASK_ID")' - IEXEC_IN: '$env.get("IEXEC_IN")' - IEXEC_OUT: '$env.get("IEXEC_OUT")' - #* dataset *# - IEXEC_DATASET_ADDRESS: '$env.get("IEXEC_DATASET_ADDRESS")' - IEXEC_DATASET_FILENAME: '$env.get("IEXEC_DATASET_FILENAME")' - #* BoT *# - IEXEC_BOT_SIZE: '$env.get("IEXEC_BOT_SIZE")' - IEXEC_BOT_FIRST_INDEX: '$env.get("IEXEC_BOT_FIRST_INDEX")' - IEXEC_BOT_TASK_INDEX: '$env.get("IEXEC_BOT_TASK_INDEX")' - #* input files *# - IEXEC_INPUT_FILES_FOLDER: '$env.get("IEXEC_INPUT_FILES_FOLDER")' - IEXEC_INPUT_FILES_NUMBER: '$env.get("IEXEC_INPUT_FILES_NUMBER")' - #foreach($key in $INPUT_FILE_NAMES.keySet()) - $key: '$INPUT_FILE_NAMES.get($key)' - #end - IEXEC_APP_DEVELOPER_SECRET: '$IEXEC_APP_DEVELOPER_SECRET_1' - IEXEC_APP_DEVELOPER_SECRET_1: '$IEXEC_APP_DEVELOPER_SECRET_1' - #foreach($key in $REQUESTER_SECRETS.keySet()) - $key: '$REQUESTER_SECRETS.get($key)' - #end - - #* - Post-compute enclave - *# - - name: 'post-compute' - image_name: 'post-compute-image' - mrenclaves: ['$POST_COMPUTE_MRENCLAVE'] - pwd: '/' - command: '$POST_COMPUTE_ENTRYPOINT' - environment: - LD_LIBRARY_PATH: '/usr/lib/jvm/java-11-openjdk/lib/server:/usr/lib/jvm/java-11-openjdk/lib:/usr/lib/jvm/java-11-openjdk/../lib' - JAVA_TOOL_OPTIONS: '-Xmx256m' - RESULT_TASK_ID: '$RESULT_TASK_ID' - RESULT_ENCRYPTION: '$RESULT_ENCRYPTION' - RESULT_ENCRYPTION_PUBLIC_KEY: '$RESULT_ENCRYPTION_PUBLIC_KEY' - RESULT_STORAGE_PROVIDER: '$RESULT_STORAGE_PROVIDER' - RESULT_STORAGE_PROXY: '$RESULT_STORAGE_PROXY' - RESULT_STORAGE_TOKEN: '$RESULT_STORAGE_TOKEN' - RESULT_STORAGE_CALLBACK: '$RESULT_STORAGE_CALLBACK' - RESULT_SIGN_WORKER_ADDRESS: '$RESULT_SIGN_WORKER_ADDRESS' - RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY: '$RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY' - -#** - Images used by each service -*# -images: - #* pre-compute. Only when dataset is requested *# - #if ($IS_PRE_COMPUTE_REQUIRED) - - name: pre-compute-image - volumes: - - name: 'iexec_in' - path: '/iexec_in' - #end - - #* application *# - - name: app-image - volumes: - - name: 'iexec_in' - path: '/iexec_in' - - name: 'iexec_out' - path: '/iexec_out' - - #* post-compute *# - - name: post-compute-image - volumes: - - name: 'iexec_out' - path: '/iexec_out' - - name: 'post-compute-tmp' - path: '/post-compute-tmp' - -#** - Volumes that will be protected - for each service. -*# -volumes: - - name: iexec_in - - name: iexec_out - - name: post-compute-tmp - -security: - attestation: - tolerate: [$TOLERATED_INSECURE_OPTIONS] - ignore_advisories: [$IGNORED_SGX_ADVISORIES] diff --git a/src/main/resources/sms-palaemon-conf.yml.template b/src/main/resources/sms-palaemon-conf.yml.template deleted file mode 100644 index 781dba24..00000000 --- a/src/main/resources/sms-palaemon-conf.yml.template +++ /dev/null @@ -1,34 +0,0 @@ -## Palaemon config ## -name: s1 -digest: create - -services: - - name: @IEXEC_SMS_PALAEMON_SERVICE_NAME@ - image_name: image3 - mrenclaves: [@IEXEC_SMS_MRENCLAVE@] - tags: [demo] - pwd: / - command: java -jar /app/iexec-sms.jar - fspf_path: /fspf.pb - fspf_key: @IEXEC_SMS_FSPF_KEY@ - fspf_tag: @IEXEC_SMS_FSPF_TAG@ - environment: - LD_LIBRARY_PATH: '/usr/lib/jvm/java-11-openjdk/lib/server:/usr/lib/jvm/java-11-openjdk/lib:/usr/lib/jvm/java-11-openjdk/../lib' - JAVA_TOOL_OPTIONS: -Xmx256m - IEXEC_SMS_BLOCKCHAIN_NODE_ADDRESS: http://chain:8545 - IEXEC_SCONE_CAS_HOST: iexec-cas - IEXEC_PALAEMON_TEMPLATE: /palaemonTemplate.vm - IEXEC_SMS_SSL_KEYSTORE: /ssl-keystore-dev.p12 - IEXEC_SMS_STORAGE_ENCRYPTION_AES_KEY_PATH: /scone/iexec-sms-aes.key - IEXEC_SMS_H2_URL: jdbc:h2:file:/scone/sms-h2 - -images: - - name: image3 - mrenclaves: [@IEXEC_SMS_MRENCLAVE@] - tags: [demo] - volumes: - - name: scone - path: /scone - -volumes: - - name: scone diff --git a/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java b/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java index 67322d33..0ff228b8 100644 --- a/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java +++ b/src/test/java/com/iexec/sms/authorization/AuthorizationServiceTests.java @@ -16,21 +16,12 @@ package com.iexec.sms.authorization; -import static com.iexec.common.chain.ChainTaskStatus.ACTIVE; -import static com.iexec.common.chain.ChainTaskStatus.UNSET; -import static com.iexec.sms.authorization.AuthorizationError.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -import java.util.Optional; - import com.iexec.common.chain.ChainDeal; import com.iexec.common.chain.ChainTask; import com.iexec.common.chain.WorkerpoolAuthorization; import com.iexec.common.security.Signature; import com.iexec.common.utils.TestUtils; import com.iexec.sms.blockchain.IexecHubService; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,6 +29,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + +import static com.iexec.common.chain.ChainTaskStatus.ACTIVE; +import static com.iexec.common.chain.ChainTaskStatus.UNSET; +import static com.iexec.sms.authorization.AuthorizationError.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + class AuthorizationServiceTests { @Mock @@ -169,7 +168,6 @@ void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenTaskNotActiveWithDetails() { @Test void shouldNotBeAuthorizedOnExecutionOfTeeTaskWhenGetDealFailedWithDetails() { - ChainDeal chainDeal = TestUtils.getChainDeal(); ChainTask chainTask = TestUtils.getChainTask(ACTIVE); WorkerpoolAuthorization auth = TestUtils.getTeeWorkerpoolAuth(); auth.setSignature(new Signature(TestUtils.POOL_WRONG_SIGNATURE)); diff --git a/src/test/java/com/iexec/sms/secret/SecretControllerTests.java b/src/test/java/com/iexec/sms/secret/SecretControllerTests.java new file mode 100644 index 00000000..e644d7c1 --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/SecretControllerTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret; + +import com.iexec.sms.authorization.AuthorizationService; +import com.iexec.sms.secret.web2.*; +import com.iexec.sms.secret.web3.Web3SecretService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class SecretControllerTests { + + private static final String AUTHORIZATION = "AUTHORIZATION"; + private static final String CHALLENGE = "CHALLENGE"; + private static final String WEB2_OWNER_ADDRESS = "WEB2_OWNER_ADDRESS"; + private static final String WEB2_SECRET_NAME = "WEB2_SECRET_NAME"; + private static final String WEB2_SECRET_VALUE = "WEB2_SECRET_VALUE"; + private static final String WEB3_SECRET_ADDRESS = "WEB3_SECRET_ADDRESS"; + private static final String WEB3_SECRET_VALUE = "WEB3_SECRET_VALUE"; + + @Mock + private AuthorizationService authorizationService; + + @Mock + private Web2SecretService web2SecretService; + + @Mock + private Web3SecretService web3SecretService; + + @InjectMocks + private SecretController secretController; + + private static final SecureRandom seed = new SecureRandom(); + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + } + + //region isWeb3SecretSet + @Test + void shouldReturnNoContentWhenWeb3SecretExists() { + when(web3SecretService.isSecretPresent(WEB3_SECRET_ADDRESS)) + .thenReturn(true); + assertThat(secretController.isWeb3SecretSet(WEB3_SECRET_ADDRESS)) + .isEqualTo(ResponseEntity.noContent().build()); + verifyNoInteractions(authorizationService, web2SecretService); + } + + @Test + void shouldReturnNotFoundWhenWeb3SecretDoesNotExist() { + when(web3SecretService.isSecretPresent(WEB3_SECRET_ADDRESS)) + .thenReturn(false); + assertThat(secretController.isWeb3SecretSet(WEB3_SECRET_ADDRESS)) + .isEqualTo(ResponseEntity.notFound().build()); + verifyNoInteractions(authorizationService, web2SecretService); + } + //endregion + + //region addWeb3Secret + @Test + void failToAddWeb3SecretWhenPayloadTooLarge() { + assertThat(secretController.addWeb3Secret(AUTHORIZATION, WEB3_SECRET_ADDRESS, getRandomString(4097))) + .isEqualTo(ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build()); + verifyNoInteractions(authorizationService, web2SecretService, web3SecretService); + } + + @Test + void failToAddWeb3SecretWhenBadAuthorization() { + when(authorizationService.getChallengeForSetWeb3Secret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, WEB3_SECRET_ADDRESS)) + .thenReturn(false); + assertThat(secretController.addWeb3Secret(AUTHORIZATION, WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .isEqualTo(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + verifyNoInteractions(web2SecretService, web3SecretService); + } + + @Test + void failToAddWeb3SecretWhenSecretAlreadyExists() { + when(authorizationService.getChallengeForSetWeb3Secret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, WEB3_SECRET_ADDRESS)) + .thenReturn(true); + when(web3SecretService.addSecret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .thenReturn(false); + assertThat(secretController.addWeb3Secret(AUTHORIZATION, WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .isEqualTo(ResponseEntity.status(HttpStatus.CONFLICT).build()); + verifyNoInteractions(web2SecretService); + verify(web3SecretService).addSecret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE); + } + + @Test + void addWeb3Secret() { + when(authorizationService.getChallengeForSetWeb3Secret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByOwner(CHALLENGE, AUTHORIZATION, WEB3_SECRET_ADDRESS)) + .thenReturn(true); + when(web3SecretService.addSecret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .thenReturn(true); + assertThat(secretController.addWeb3Secret(AUTHORIZATION, WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE)) + .isEqualTo(ResponseEntity.noContent().build()); + verifyNoInteractions(web2SecretService); + verify(web3SecretService).addSecret(WEB3_SECRET_ADDRESS, WEB3_SECRET_VALUE); + } + //endregion + + //region isWeb2SecretSet + @Test + void shouldReturnNoContentWhenWeb2SecretExists() { + when(web2SecretService.isSecretPresent(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME)) + .thenReturn(true); + assertThat(secretController.isWeb2SecretSet(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME)) + .isEqualTo(ResponseEntity.noContent().build()); + verifyNoInteractions(authorizationService, web3SecretService); + } + + @Test + void shouldReturnNotFoundWhenWeb2SecretDoesNotExist() { + when(web2SecretService.isSecretPresent(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME)) + .thenReturn(false); + assertThat(secretController.isWeb2SecretSet(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME)) + .isEqualTo(ResponseEntity.notFound().build()); + verifyNoInteractions(authorizationService, web3SecretService); + } + //endregion + + //region addWeb2Secret + @Test + void failToAddWeb2SecretWhenPayloadTooLarge() { + assertThat(secretController.addWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, getRandomString(4097))) + .isEqualTo(ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build()); + verifyNoInteractions(web2SecretService, web3SecretService); + } + + @Test + void failToAddWeb2SecretWhenBadAuthorization() { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(false); + assertThat(secretController.addWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + verifyNoInteractions(web2SecretService, web3SecretService); + } + + @Test + void failToAddWeb2SecretWhenSecretAlreadyExists() throws SecretAlreadyExistsException { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(true); + when(web2SecretService.addSecret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenThrow(new SecretAlreadyExistsException(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME)); + assertThat(secretController.addWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.status(HttpStatus.CONFLICT).build()); + verify(web2SecretService).addSecret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE); + } + + @Test + void addWeb2Secret() throws SecretAlreadyExistsException { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(true); + when(web2SecretService.addSecret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(new Web2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)); + assertThat(secretController.addWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.noContent().build()); + verify(web2SecretService).addSecret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE); + } + //endregion + + //region updateWeb2Secret + @Test + void failToUpdateWeb2SecretWhenPayloadTooLarge() { + assertThat(secretController.updateWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, getRandomString(4097))) + .isEqualTo(ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).build()); + verifyNoInteractions(web2SecretService, web3SecretService); + } + + @Test + void failToUpdateWeb2SecretWhenBadAuthorization() { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(false); + assertThat(secretController.updateWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()); + verifyNoInteractions(web2SecretService, web3SecretService); + } + + @Test + void failToUpdateWeb2SecretWhenSecretIsMissing() throws NotAnExistingSecretException, SameSecretException { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(true); + doThrow(NotAnExistingSecretException.class).when(web2SecretService) + .updateSecret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE); + assertThat(secretController.updateWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.notFound().build()); + } + + @Test + void updateWeb2Secret() { + when(authorizationService.getChallengeForSetWeb2Secret(WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .thenReturn(CHALLENGE); + when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WEB2_OWNER_ADDRESS)) + .thenReturn(true); + assertThat(secretController.updateWeb2Secret(AUTHORIZATION, WEB2_OWNER_ADDRESS, WEB2_SECRET_NAME, WEB2_SECRET_VALUE)) + .isEqualTo(ResponseEntity.noContent().build()); + } + //endregion + + String getRandomString(int size) { + byte[] bytes = new byte[size]; + seed.nextBytes(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + +} diff --git a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeaderTests.java b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeaderTests.java new file mode 100644 index 00000000..c6088d3c --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretHeaderTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret.compute; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import javax.validation.ValidationException; + +class TeeTaskComputeSecretHeaderTests { + private final static String ON_CHAIN_OBJECT_ADDRESS = "onChainObjectAddress"; + private final static String FIXED_SECRET_OWNER = "fixedSecretOwner"; + private final static String KEY = "key"; + + // region Valid constructions + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldConstructNewApplicationDeveloperSecretHeader(OnChainObjectType objectType) { + Assertions.assertThatNoException().isThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + ON_CHAIN_OBJECT_ADDRESS, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + KEY + )); + } + + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldConstructNewRequesterSecretHeader(OnChainObjectType objectType) { + Assertions.assertThatNoException().isThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + "", + SecretOwnerRole.REQUESTER, + FIXED_SECRET_OWNER, + KEY + )); + } + // endregion + + // region Invalid construction + @Test + void shouldNotConstructNewSecretHeaderBecauseOnChainObjectTypeIsNull() { + Assertions.assertThatThrownBy(() -> new TeeTaskComputeSecretHeader( + null, + ON_CHAIN_OBJECT_ADDRESS, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + KEY + )).isInstanceOf(ValidationException.class); + } + + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldNotConstructSecretHeaderBecauseSecretOwnerRoleIsNull(OnChainObjectType objectType) { + Assertions.assertThatThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + ON_CHAIN_OBJECT_ADDRESS, + null, + FIXED_SECRET_OWNER, + KEY + )).isInstanceOf(ValidationException.class); + } + + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldNotConstructSecretHeaderBecauseKeyIsNull(OnChainObjectType objectType) { + Assertions.assertThatThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + ON_CHAIN_OBJECT_ADDRESS, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + null + )).isInstanceOf(ValidationException.class); + } + + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldNotConstructNewApplicationDeveloperSecretHeaderBecauseFixedSecretOwnerNotEmpty(OnChainObjectType objectType) { + Assertions.assertThatThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + ON_CHAIN_OBJECT_ADDRESS, + SecretOwnerRole.APPLICATION_DEVELOPER, + FIXED_SECRET_OWNER, + KEY + )).isInstanceOf(ValidationException.class); + } + + @ParameterizedTest + @EnumSource(OnChainObjectType.class) + void shouldNotConstructNewRequesterSecretHeaderBecauseOnChainObjectAddressNotEmpty(OnChainObjectType objectType) { + Assertions.assertThatThrownBy(() -> new TeeTaskComputeSecretHeader( + objectType, + ON_CHAIN_OBJECT_ADDRESS, + SecretOwnerRole.REQUESTER, + "", + KEY + )).isInstanceOf(ValidationException.class); + } + // endregion +} diff --git a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java index f586952a..a9434c58 100644 --- a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java +++ b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretServiceTest.java @@ -19,6 +19,7 @@ class TeeTaskComputeSecretServiceTest { .onChainObjectType(OnChainObjectType.APPLICATION) .onChainObjectAddress(APP_ADDRESS.toLowerCase()) .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") .key("0") .value(ENCRYPTED_SECRET_VALUE) .build(); @@ -53,8 +54,8 @@ void shouldAddSecret() { verify(teeTaskComputeSecretRepository, times(1)).save(computeSecretCaptor.capture()); final TeeTaskComputeSecret savedTeeTaskComputeSecret = computeSecretCaptor.getValue(); - Assertions.assertThat(savedTeeTaskComputeSecret.getKey()).isEqualTo("0"); - Assertions.assertThat(savedTeeTaskComputeSecret.getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); + Assertions.assertThat(savedTeeTaskComputeSecret.getHeader().getKey()).isEqualTo("0"); + Assertions.assertThat(savedTeeTaskComputeSecret.getHeader().getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); Assertions.assertThat(savedTeeTaskComputeSecret.getValue()).isEqualTo(ENCRYPTED_SECRET_VALUE); } @@ -72,15 +73,15 @@ void shouldNotAddSecretSinceAlreadyExist() { // region getSecret @Test void shouldGetSecret() { - when(teeTaskComputeSecretRepository.findOne(any())) + when(teeTaskComputeSecretRepository.findById(any())) .thenReturn(Optional.of(COMPUTE_SECRET)); when(encryptionService.decrypt(ENCRYPTED_SECRET_VALUE)) .thenReturn(DECRYPTED_SECRET_VALUE); Optional decryptedSecret = teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, APP_ADDRESS, SecretOwnerRole.APPLICATION_DEVELOPER, "", "0"); Assertions.assertThat(decryptedSecret).isPresent(); - Assertions.assertThat(decryptedSecret.get().getKey()).isEqualTo("0"); - Assertions.assertThat(decryptedSecret.get().getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); + Assertions.assertThat(decryptedSecret.get().getHeader().getKey()).isEqualTo("0"); + Assertions.assertThat(decryptedSecret.get().getHeader().getOnChainObjectAddress()).isEqualTo(APP_ADDRESS.toLowerCase()); Assertions.assertThat(decryptedSecret.get().getValue()).isEqualTo(DECRYPTED_SECRET_VALUE); verify(encryptionService, Mockito.times(1)).decrypt(any()); } diff --git a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java index 8fff237f..c4a2be1f 100644 --- a/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java +++ b/src/test/java/com/iexec/sms/secret/compute/TeeTaskComputeSecretTest.java @@ -1,17 +1,14 @@ package com.iexec.sms.secret.compute; -import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.dao.DataIntegrityViolationException; import javax.validation.ConstraintViolationException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -@Slf4j @DataJpaTest public class TeeTaskComputeSecretTest { @@ -43,85 +40,6 @@ private TeeTaskComputeSecret getRequesterSecret() { .build(); } - @Test - void shouldNotSaveSecretFromDefaultBuilder() { - log.info("shouldNotSaveEntity"); - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder().build(); - assertThat(secret).hasAllNullFieldsOrProperties(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - - @Test - void shouldNotSaveSecretWhenOnChainObjectAddressIsNull() { - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() - //.onChainObjectAddress("") - .onChainObjectType(OnChainObjectType.APPLICATION) - .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - .fixedSecretOwner("") - .key("") - .value("") - .build(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - - @Test - void shouldNotSaveSecretWhenOnChainObjectTypeIsNull() { - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() - .onChainObjectAddress("") - //.onChainObjectType(OnChainObjectType.APPLICATION) - .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - .fixedSecretOwner("") - .key("") - .value("") - .build(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - - @Test - void shouldNotSaveSecretWhenSecretOwnerRoleIsNull() { - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() - .onChainObjectAddress("") - .onChainObjectType(OnChainObjectType.APPLICATION) - //.secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - .fixedSecretOwner("") - .key("") - .value("") - .build(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - - @Test - void shouldNotSaveSecretWhenFixedSecretOwnerIsNull() { - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() - .onChainObjectAddress("") - .onChainObjectType(OnChainObjectType.APPLICATION) - .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - //.fixedSecretOwner("") - .key("") - .value("") - .build(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - - @Test - void shouldNotSaveSecretWhenKeyIsNull() { - TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() - .onChainObjectAddress("") - .onChainObjectType(OnChainObjectType.APPLICATION) - .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - .fixedSecretOwner("") - //.key("") - .value("") - .build(); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) - .isInstanceOf(ConstraintViolationException.class); - } - @Test void shouldNotSaveSecretWhenValueIsNull() { TeeTaskComputeSecret secret = TeeTaskComputeSecret.builder() @@ -129,41 +47,25 @@ void shouldNotSaveSecretWhenValueIsNull() { .onChainObjectType(OnChainObjectType.APPLICATION) .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) .fixedSecretOwner("") - .key("") + .key("key") //.value("") .build(); assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(secret)) .isInstanceOf(ConstraintViolationException.class); } - @Test - void shouldFailToSaveSameAppDeveloperSecretTwice() { - log.info("AppDeveloperSecret"); - teeTaskComputeSecretRepository.saveAndFlush(getAppDeveloperSecret()); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(getAppDeveloperSecret())) - .isInstanceOf(DataIntegrityViolationException.class); - } - - @Test - void shouldFailToSaveSameRequesterSecretTwice() { - log.info("RequesterSecret"); - teeTaskComputeSecretRepository.saveAndFlush(getRequesterSecret()); - assertThatThrownBy(() -> teeTaskComputeSecretRepository.saveAndFlush(getRequesterSecret())) - .isInstanceOf(DataIntegrityViolationException.class); - } - @Test void shouldSaveAppDeveloperAndRequesterSecrets() { TeeTaskComputeSecret appDeveloperSecret = getAppDeveloperSecret(); teeTaskComputeSecretRepository.save(appDeveloperSecret); - String appDeveloperSecretId = appDeveloperSecret.getId(); + final TeeTaskComputeSecretHeader appDeveloperSecretHeader = appDeveloperSecret.getHeader(); TeeTaskComputeSecret requesterSecret = getRequesterSecret(); teeTaskComputeSecretRepository.save(requesterSecret); - String requesterSecretId = requesterSecret.getId(); + final TeeTaskComputeSecretHeader requesterSecretHeader = requesterSecret.getHeader(); assertThat(teeTaskComputeSecretRepository.count()).isEqualTo(2); - assertThat(teeTaskComputeSecretRepository.getById(appDeveloperSecretId)) + assertThat(teeTaskComputeSecretRepository.getById(appDeveloperSecretHeader)) .isEqualTo(appDeveloperSecret); - assertThat(teeTaskComputeSecretRepository.getById(requesterSecretId)) + assertThat(teeTaskComputeSecretRepository.getById(requesterSecretHeader)) .isEqualTo(requesterSecret); } diff --git a/src/test/java/com/iexec/sms/secret/web2/Web2SecretServiceTests.java b/src/test/java/com/iexec/sms/secret/web2/Web2SecretServiceTests.java new file mode 100644 index 00000000..8b5e0e4d --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/web2/Web2SecretServiceTests.java @@ -0,0 +1,199 @@ +/* + * + * * Copyright 2022 IEXEC BLOCKCHAIN TECH + * * + * * 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. + * + */ + +package com.iexec.sms.secret.web2; + +import com.iexec.sms.encryption.EncryptionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class Web2SecretServiceTests { + private static final String OWNER_ADDRESS = "ownerAddress"; + private static final String SECRET_ADDRESS = "secretAddress"; + private static final String PLAIN_SECRET_VALUE = "plainSecretValue"; + private static final String ENCRYPTED_SECRET_VALUE = "encryptedSecretValue"; + + @Mock + private Web2SecretRepository web2SecretRepository; + + @Mock + private EncryptionService encryptionService; + + @InjectMocks + private Web2SecretService web2SecretService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + } + + // region getSecret + @Test + void shouldGetDecryptedValue() { + final Web2Secret encryptedSecret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE); + + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.of(encryptedSecret)); + when(encryptionService.decrypt(ENCRYPTED_SECRET_VALUE)) + .thenReturn(PLAIN_SECRET_VALUE); + + final Optional result = web2SecretService.getDecryptedValue(OWNER_ADDRESS, SECRET_ADDRESS); + assertThat(result) + .isNotEmpty() + .get().isEqualTo(PLAIN_SECRET_VALUE); + } + + @Test + void shouldGetEncryptedSecret() { + final Web2Secret encryptedSecret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE); + + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.of(encryptedSecret)); + + final Optional result = web2SecretService.getSecret(OWNER_ADDRESS, SECRET_ADDRESS); + assertThat(result).isNotEmpty(); + assertAll( + () -> assertThat(result).get().extracting(Web2Secret::getHeader).usingRecursiveComparison().isEqualTo(new Web2SecretHeader(OWNER_ADDRESS, SECRET_ADDRESS)), + () -> assertThat(result).get().extracting(Web2Secret::getValue).isEqualTo(ENCRYPTED_SECRET_VALUE), + () -> verifyNoInteractions(encryptionService) + ); + } + + @Test + void shouldGetEmptySecretIfSecretNotPresent() { + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.empty()); + + assertThat(web2SecretService.getSecret(OWNER_ADDRESS, SECRET_ADDRESS)).isEmpty(); + verify(web2SecretRepository, times(1)) + .findById(any(Web2SecretHeader.class)); + verifyNoInteractions(encryptionService); + } + + @Test + void shouldGetEmptyDecryptedValueIfSecretNotPresent() { + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.empty()); + + assertThat(web2SecretService.getDecryptedValue(OWNER_ADDRESS, SECRET_ADDRESS)).isEmpty(); + verify(web2SecretRepository, times(1)) + .findById(any(Web2SecretHeader.class)); + verifyNoInteractions(encryptionService); + } + // endregion + + // region addSecret + @Test + void shouldAddSecret() throws SecretAlreadyExistsException { + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.empty()); + when(encryptionService.encrypt(PLAIN_SECRET_VALUE)).thenReturn(ENCRYPTED_SECRET_VALUE); + when(web2SecretRepository.save(any())).thenReturn(new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE)); + + final Web2Secret newSecret = web2SecretService.addSecret(OWNER_ADDRESS, SECRET_ADDRESS, PLAIN_SECRET_VALUE); + assertAll( + () -> assertThat(newSecret).extracting(Web2Secret::getHeader).usingRecursiveComparison().isEqualTo(new Web2SecretHeader(OWNER_ADDRESS, SECRET_ADDRESS)), + () -> assertThat(newSecret).extracting(Web2Secret::getValue).isEqualTo(ENCRYPTED_SECRET_VALUE), + + () -> verify(encryptionService).encrypt(any()), + () -> verify(web2SecretRepository).save(any()) + ); + } + + @Test + void shouldNotAddSecretIfPresent() { + final Web2Secret secret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE); + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.of(secret)); + + final SecretAlreadyExistsException exception = assertThrows(SecretAlreadyExistsException.class, + () -> web2SecretService.addSecret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE)); + assertAll( + () -> assertEquals(OWNER_ADDRESS, exception.getOwnerAddress()), + () -> assertEquals(SECRET_ADDRESS, exception.getSecretAddress()), + () -> verify(encryptionService, never()).encrypt(PLAIN_SECRET_VALUE), + () -> verify(web2SecretRepository, never()).save(any()) + ); + } + // endregion + + // region updateSecret + @Test + void shouldUpdateSecret() throws NotAnExistingSecretException, SameSecretException { + final Web2Secret encryptedSecret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE); + final String newSecretValue = "newSecretValue"; + final String newEncryptedSecretValue = "newEncryptedSecretValue"; + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.of(encryptedSecret)); + when(encryptionService.encrypt(newSecretValue)) + .thenReturn(newEncryptedSecretValue); + final Web2Secret savedSecret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, newEncryptedSecretValue); + when(web2SecretRepository.save(any())) + .thenReturn(savedSecret); + + final Web2Secret newSecret = web2SecretService.updateSecret(OWNER_ADDRESS, SECRET_ADDRESS, newSecretValue); + assertAll( + () -> assertThat(newSecret).extracting(Web2Secret::getHeader).usingRecursiveComparison().isEqualTo(new Web2SecretHeader(OWNER_ADDRESS, SECRET_ADDRESS)), + () -> assertThat(newSecret).extracting(Web2Secret::getValue).isEqualTo(newEncryptedSecretValue), + + () -> verify(web2SecretRepository, never()).save(encryptedSecret), // Current object should not be updated + () -> verify(web2SecretRepository, times(1)).save(any()) // A new object should be created with the same ID + ); + } + + @Test + void shouldNotUpdateSecretIfMissing() { + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.empty()); + + final NotAnExistingSecretException exception = assertThrows(NotAnExistingSecretException.class, + () -> web2SecretService.updateSecret(OWNER_ADDRESS, SECRET_ADDRESS, PLAIN_SECRET_VALUE)); + assertAll( + () -> assertEquals(OWNER_ADDRESS, exception.getOwnerAddress()), + () -> assertEquals(SECRET_ADDRESS, exception.getSecretAddress()), + () -> verify(web2SecretRepository, never()).save(any()) + ); + } + + @Test + void shouldNotUpdateSecretIfSameValue() { + final Web2Secret encryptedSecret = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, ENCRYPTED_SECRET_VALUE); + when(web2SecretRepository.findById(any(Web2SecretHeader.class))) + .thenReturn(Optional.of(encryptedSecret)); + when(encryptionService.encrypt(PLAIN_SECRET_VALUE)) + .thenReturn(ENCRYPTED_SECRET_VALUE); + + final SameSecretException exception = assertThrows(SameSecretException.class, + () -> web2SecretService.updateSecret(OWNER_ADDRESS, SECRET_ADDRESS, PLAIN_SECRET_VALUE)); + assertAll( + () -> assertEquals(OWNER_ADDRESS, exception.getOwnerAddress()), + () -> assertEquals(SECRET_ADDRESS, exception.getSecretAddress()), + () -> verify(web2SecretRepository, never()).save(any()) + ); + } + // endregion +} diff --git a/src/test/java/com/iexec/sms/secret/web2/Web2SecretTests.java b/src/test/java/com/iexec/sms/secret/web2/Web2SecretTests.java new file mode 100644 index 00000000..6be72f64 --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/web2/Web2SecretTests.java @@ -0,0 +1,39 @@ +/* + * + * * Copyright 2022 IEXEC BLOCKCHAIN TECH + * * + * * 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. + * + */ + +package com.iexec.sms.secret.web2; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class Web2SecretTests { + private static final String OWNER_ADDRESS = "ownerAddress"; + private static final String SECRET_ADDRESS = "secretAddress"; + + private static final String VALUE = "value"; + private static final String NEW_VALUE = "newValue"; + private static final Web2Secret SECRET = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, VALUE); + private static final Web2Secret NEW_SECRET = new Web2Secret(OWNER_ADDRESS, SECRET_ADDRESS, NEW_VALUE); + + @Test + void withValue() { + Assertions.assertThat(SECRET.withValue(NEW_VALUE)) + .usingRecursiveComparison() + .isEqualTo(NEW_SECRET); + } +} diff --git a/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java b/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java deleted file mode 100644 index 1d17bec1..00000000 --- a/src/test/java/com/iexec/sms/secret/web2/Web2SecretsServiceTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.secret.web2; - -import com.iexec.sms.encryption.EncryptionService; -import com.iexec.sms.secret.Secret; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -class Web2SecretsServiceTests { - - String ownerAddress = "ownerAddress"; - String secretAddress = "secretAddress"; - String plainSecretValue = "plainSecretValue"; - String encryptedSecretValue = "encryptedSecretValue"; - - @Mock - private Web2SecretsRepository web2SecretsRepository; - - @Mock - private EncryptionService encryptionService; - - @InjectMocks - private Web2SecretsService web2SecretsService; - - @BeforeEach - void beforeEach() { - MockitoAnnotations.openMocks(this); - } - - @Test - void shouldGetAndDecryptWeb2Secrets() { - ownerAddress = ownerAddress.toLowerCase(); - Secret encryptedSecret = new Secret(secretAddress, encryptedSecretValue); - encryptedSecret.setEncryptedValue(true); - List secretList = List.of(encryptedSecret); - Web2Secrets web2SecretsMock = new Web2Secrets(ownerAddress); - web2SecretsMock.setSecrets(secretList); - when(web2SecretsRepository.findWeb2SecretsByOwnerAddress(ownerAddress)) - .thenReturn(Optional.of(web2SecretsMock)); - when(encryptionService.decrypt(encryptedSecret.getValue())) - .thenReturn(plainSecretValue); - - Optional result = web2SecretsService.getSecret(ownerAddress, secretAddress, true); - assertThat(result.get().getAddress()).isEqualTo(secretAddress); - assertThat(result.get().getValue()).isEqualTo(plainSecretValue); - } - - @Test - void shouldAddSecret() { - ownerAddress = ownerAddress.toLowerCase(); - web2SecretsService.addSecret(ownerAddress, secretAddress, plainSecretValue); - verify(web2SecretsRepository, times(1)).save(any()); - } - - @Test - void shouldUpdateSecret() { - ownerAddress = ownerAddress.toLowerCase(); - Secret encryptedSecret = new Secret(secretAddress, encryptedSecretValue); - encryptedSecret.setEncryptedValue(true); - String newSecretValue = "newSecretValue"; - String newEncryptedSecretValue = "newEncryptedSecretValue"; - List secretList = List.of(encryptedSecret); - Web2Secrets web2SecretsMock = new Web2Secrets(ownerAddress); - web2SecretsMock.setSecrets(secretList); - when(web2SecretsRepository.findWeb2SecretsByOwnerAddress(ownerAddress)) - .thenReturn(Optional.of(web2SecretsMock)); - when(encryptionService.encrypt(newSecretValue)) - .thenReturn(newEncryptedSecretValue); - - web2SecretsService.updateSecret(ownerAddress, secretAddress, newSecretValue); - verify(web2SecretsRepository, times(1)).save(web2SecretsMock); - } -} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/secret/web3/Web3SecretServiceTests.java b/src/test/java/com/iexec/sms/secret/web3/Web3SecretServiceTests.java new file mode 100644 index 00000000..42b73461 --- /dev/null +++ b/src/test/java/com/iexec/sms/secret/web3/Web3SecretServiceTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.secret.web3; + +import com.iexec.sms.encryption.EncryptionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class Web3SecretServiceTests { + + String secretAddress = "secretAddress".toLowerCase(); + String plainSecretValue = "plainSecretValue"; + String encryptedSecretValue = "encryptedSecretValue"; + + @Mock + private EncryptionService encryptionService; + @Mock + private Web3SecretRepository web3SecretRepository; + @InjectMocks + private Web3SecretService web3SecretService; + + @BeforeEach + void init() { + MockitoAnnotations.openMocks(this); + } + + @Test + void shouldNotAddSecretIfPresent() { + Web3Secret web3Secret = new Web3Secret(secretAddress, encryptedSecretValue); + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.of(web3Secret)); + assertThat(web3SecretService.addSecret(secretAddress, plainSecretValue)).isFalse(); + verifyNoInteractions(encryptionService); + verify(web3SecretRepository, never()).save(any()); + } + + @Test + void shouldAddSecret() { + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.empty()); + when(encryptionService.encrypt(plainSecretValue)).thenReturn(encryptedSecretValue); + assertThat(web3SecretService.addSecret(secretAddress, plainSecretValue)).isTrue(); + verify(encryptionService).encrypt(any()); + verify(web3SecretRepository).save(any()); + } + + @Test + void shouldGetDecryptedValue() { + Web3Secret encryptedSecret = new Web3Secret(secretAddress, encryptedSecretValue); + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.of(encryptedSecret)); + when(encryptionService.decrypt(encryptedSecretValue)).thenReturn(plainSecretValue); + + Optional result = web3SecretService.getDecryptedValue(secretAddress); + assertThat(result) + .isPresent() + .get().isEqualTo(plainSecretValue); + + verify(web3SecretRepository).findById(any(Web3SecretHeader.class)); + verify(encryptionService).decrypt(any()); + } + + @Test + void shouldGetEncryptedSecret() { + Web3Secret encryptedSecret = new Web3Secret(secretAddress, encryptedSecretValue); + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.of(encryptedSecret)); + Optional result = web3SecretService.getSecret(secretAddress); + assertThat(result) + .contains(encryptedSecret); + verify(web3SecretRepository, times(1)).findById(any(Web3SecretHeader.class)); + verifyNoInteractions(encryptionService); + } + + @Test + void shouldGetEmptySecretIfSecretNotPresent() { + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.empty()); + assertThat(web3SecretService.getSecret(secretAddress)).isEmpty(); + verify(web3SecretRepository, times(1)).findById(any(Web3SecretHeader.class)); + verifyNoInteractions(encryptionService); + } + + @Test + void shouldGetEmptyValueIfSecretNotPresent() { + when(web3SecretRepository.findById(any(Web3SecretHeader.class))).thenReturn(Optional.empty()); + assertThat(web3SecretService.getDecryptedValue(secretAddress)).isEmpty(); + verify(web3SecretRepository, times(1)).findById(any(Web3SecretHeader.class)); + verifyNoInteractions(encryptionService); + } + +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/ConditionalOnTeeFrameworkTests.java b/src/test/java/com/iexec/sms/tee/ConditionalOnTeeFrameworkTests.java new file mode 100644 index 00000000..be828dba --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/ConditionalOnTeeFrameworkTests.java @@ -0,0 +1,189 @@ +package com.iexec.sms.tee; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.tee.session.generic.TeeSessionHandler; +import com.iexec.sms.tee.session.gramine.GramineSessionHandlerService; +import com.iexec.sms.tee.session.gramine.GramineSessionMakerService; +import com.iexec.sms.tee.session.gramine.sps.SpsConfiguration; +import com.iexec.sms.tee.session.scone.SconeSessionHandlerService; +import com.iexec.sms.tee.session.scone.SconeSessionMakerService; +import com.iexec.sms.tee.session.scone.SconeSessionSecurityConfig; +import com.iexec.sms.tee.session.scone.cas.CasClient; +import com.iexec.sms.tee.session.scone.cas.CasConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +class ConditionalOnTeeFrameworkTests { + @ConditionalOnTeeFramework(frameworks = {}) + static class NoFrameworksSet {} + + @Mock + Environment environment; + @Mock + ConditionContext context; + @Mock + AnnotationMetadata metadata; + OnTeeFrameworkCondition condition = new OnTeeFrameworkCondition(); + + @BeforeEach + void init() { + MockitoAnnotations.openMocks(this); + } + + static Stream sconeBeansClasses() { + return Stream.of( +// Arguments.of(SconeServicesProperties.class), // Can't test this bean as it is declared through a method, not a class + Arguments.of(SconeSessionHandlerService.class), + Arguments.of(SconeSessionMakerService.class), + Arguments.of(SconeSessionSecurityConfig.class), + Arguments.of(CasClient.class), + Arguments.of(CasConfiguration.class) + ); + } + + static Stream gramineBeansClasses() { + return Stream.of( +// Arguments.of(GramineServicesProperties.class), // Can't test this bean as it is declared through a method, not a class + Arguments.of(GramineSessionHandlerService.class), + Arguments.of(GramineSessionMakerService.class), + Arguments.of(SpsConfiguration.class) + ); + } + + static Stream sconeAndGramineBeansClasses() { + return Stream.of(sconeBeansClasses(), gramineBeansClasses()) + .flatMap(Function.identity()); + } + + static Stream nonAnnotatedClasses() { + return Stream.of( + Arguments.of(TeeSessionHandler.class) + ); + } + + // region shouldMatch + @ParameterizedTest + @MethodSource("sconeBeansClasses") + void shouldMatchScone(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"scone"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertTrue(condition.matches(context, metadata)); + } + + @ParameterizedTest + @MethodSource("gramineBeansClasses") + void shouldMatchGramine(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"gramine"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertTrue(condition.matches(context, metadata)); + } + + @ParameterizedTest + @MethodSource("sconeAndGramineBeansClasses") + void shouldMatchSconeAndGramine(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"scone", "gramine"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertTrue(condition.matches(context, metadata)); + } + // endregion + + // region shouldNotMatch + @ParameterizedTest + @MethodSource("sconeBeansClasses") + void shouldNotMatchSconeSinceGramineProfile(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"gramine"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertFalse(condition.matches(context, metadata)); + } + + @ParameterizedTest + @MethodSource("gramineBeansClasses") + void shouldNotMatchGramineSinceSconeProfile(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"scone"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertFalse(condition.matches(context, metadata)); + } + + @ParameterizedTest + @MethodSource("sconeAndGramineBeansClasses") + void shouldNotMatchAnySinceNoProfile(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertFalse(condition.matches(context, metadata)); + } + + @ParameterizedTest + @MethodSource("nonAnnotatedClasses") + void shouldNotMatchAnySinceNoAnnotation(Class clazz) { + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + + assertFalse(condition.matches(context, metadata)); + } + + @Test + void shouldNotMatchAnySinceNoFrameworkDefined() { + final Class clazz = NoFrameworksSet.class; + + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"scone", "gramine"}); + when(metadata.getClassName()).thenReturn(clazz.getName()); + setAttributesForMetadataMock(clazz); + + assertFalse(condition.matches(context, metadata)); + } + + @Test + void shouldNotMatchAnySinceClassDoesNotExist() { + final String className = "Not an existing class"; + + when(context.getEnvironment()).thenReturn(environment); + when(environment.getActiveProfiles()).thenReturn(new String[]{"scone", "gramine"}); + when(metadata.getClassName()).thenReturn(className); + + assertFalse(condition.matches(context, metadata)); + } + // endregion + + void setAttributesForMetadataMock(Class clazz) { + ConditionalOnTeeFramework annotation = clazz.getAnnotation(ConditionalOnTeeFramework.class); + TeeFramework[] frameworks = annotation.frameworks(); + when(metadata.getAnnotationAttributes(ConditionalOnTeeFramework.class.getName())) + .thenReturn(Map.of("frameworks", frameworks)); + } +} diff --git a/src/test/java/com/iexec/sms/tee/TeeControllerTests.java b/src/test/java/com/iexec/sms/tee/TeeControllerTests.java index 2b2d09d6..252efb71 100644 --- a/src/test/java/com/iexec/sms/tee/TeeControllerTests.java +++ b/src/test/java/com/iexec/sms/tee/TeeControllerTests.java @@ -1,14 +1,19 @@ package com.iexec.sms.tee; import com.iexec.common.chain.WorkerpoolAuthorization; -import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.common.tee.TeeFramework; import com.iexec.common.web.ApiResponseBody; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.api.TeeSessionGenerationResponse; +import com.iexec.sms.api.config.GramineServicesProperties; +import com.iexec.sms.api.config.SconeServicesProperties; +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.api.config.TeeServicesProperties; import com.iexec.sms.authorization.AuthorizationError; import com.iexec.sms.authorization.AuthorizationService; import com.iexec.sms.tee.challenge.TeeChallengeService; -import com.iexec.sms.tee.session.TeeSessionGenerationException; import com.iexec.sms.tee.session.TeeSessionService; -import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -36,6 +41,9 @@ class TeeControllerTests { private final static String AUTHORIZATION = "0x2"; private final static String CHALLENGE = "CHALLENGE"; private final static String SESSION_ID = "SESSION_ID"; + private static final String SECRET_PROVISIONING_URL = "https://secretProvisioningUrl"; + private static final String LAS_IMAGE = "lasImage"; + @Mock AuthorizationService authorizationService; @@ -44,7 +52,12 @@ class TeeControllerTests { @Mock TeeSessionService teeSessionService; @Mock - TeeWorkflowConfiguration teeWorkflowConfig; + TeeServicesProperties teeServicesConfig; + + @Mock + TeeAppProperties preComputeProperties; + @Mock + TeeAppProperties postComputeProperties; @InjectMocks TeeController teeController; @@ -54,6 +67,137 @@ void setUp() { MockitoAnnotations.openMocks(this); } + // region getTeeFramework + @Test + void shouldGetSconeFramework() { + final TeeServicesProperties properties = new SconeServicesProperties( + preComputeProperties, + postComputeProperties, + LAS_IMAGE + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeFramework(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final TeeFramework result = response.getBody(); + assertEquals(TeeFramework.SCONE, result); + } + + @Test + void shouldGetGramineFramework() { + final TeeServicesProperties properties = new GramineServicesProperties( + preComputeProperties, + postComputeProperties + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeFramework(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final TeeFramework result = response.getBody(); + assertEquals(TeeFramework.GRAMINE, result); + } + // endregion + + // region getTeeServicesConfig + @Test + void shouldGetSconeProperties() { + final TeeServicesProperties properties = new SconeServicesProperties( + preComputeProperties, + postComputeProperties, + LAS_IMAGE + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeServicesProperties(TeeFramework.SCONE); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final TeeServicesProperties result = response.getBody(); + assertNotNull(result); + assertInstanceOf(SconeServicesProperties.class, result); + assertEquals(TeeFramework.SCONE, result.getTeeFramework()); + assertEquals(preComputeProperties, result.getPreComputeProperties()); + assertEquals(postComputeProperties, result.getPostComputeProperties()); + assertEquals(postComputeProperties, result.getPostComputeProperties()); + assertEquals(LAS_IMAGE, ((SconeServicesProperties) result).getLasImage()); + } + + @Test + void shouldGetGramineProperties() { + final TeeServicesProperties properties = new GramineServicesProperties( + preComputeProperties, + postComputeProperties + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeServicesProperties(TeeFramework.GRAMINE); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + + final TeeServicesProperties result = response.getBody(); + assertNotNull(result); + assertInstanceOf(GramineServicesProperties.class, result); + assertEquals(TeeFramework.GRAMINE, result.getTeeFramework()); + assertEquals(preComputeProperties, result.getPreComputeProperties()); + assertEquals(postComputeProperties, result.getPostComputeProperties()); + } + + @Test + void shouldNotGetSconePropertiesSinceGramineSms() { + final TeeServicesProperties properties = new SconeServicesProperties( + preComputeProperties, + postComputeProperties, + LAS_IMAGE + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeServicesProperties(TeeFramework.GRAMINE); + + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + + @Test + void shouldNotGetGraminePropertiesSinceSconeSms() { + final TeeServicesProperties properties = new GramineServicesProperties( + preComputeProperties, + postComputeProperties + ); + + final TeeController teeController = new TeeController( + authorizationService, teeChallengeService, teeSessionService, properties + ); + + final ResponseEntity response = + teeController.getTeeServicesProperties(TeeFramework.SCONE); + + assertEquals(HttpStatus.CONFLICT, response.getStatusCode()); + } + // endregion + // region generateTeeSession @Test void shouldGenerateTeeSession() throws TeeSessionGenerationException { @@ -66,15 +210,18 @@ void shouldGenerateTeeSession() throws TeeSessionGenerationException { when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); - when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); - when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenReturn(SESSION_ID); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)) + .thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)) + .thenReturn(new TeeSessionGenerationResponse(SESSION_ID, SECRET_PROVISIONING_URL)); - final ResponseEntity> response = - teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + final ResponseEntity> response = teeController + .generateTeeSession(AUTHORIZATION, workerpoolAuthorization); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotEquals(null, response.getBody()); - assertNotEquals(null, response.getBody().getData()); - assertNotEquals("", response.getBody().getData()); + assertNotNull(response.getBody()); + assertNotNull(response.getBody().getData()); + assertEquals(SESSION_ID, response.getBody().getData().getSessionId()); + assertEquals(SECRET_PROVISIONING_URL, response.getBody().getData().getSecretProvisioningUrl()); assertNull(response.getBody().getError()); } @@ -90,8 +237,8 @@ void shouldNotGenerateTeeSessionSinceNotSignedByHimself() { when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(false); - final ResponseEntity> response = - teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + final ResponseEntity> response = teeController + .generateTeeSession(AUTHORIZATION, workerpoolAuthorization); assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); assertNotEquals(null, response.getBody()); assertNull(response.getBody().getData()); @@ -106,13 +253,13 @@ private static Stream notAuthorizedParams() { Arguments.of(GET_CHAIN_TASK_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_TASK_FAILED), Arguments.of(TASK_NOT_ACTIVE, EXECUTION_NOT_AUTHORIZED_TASK_NOT_ACTIVE), Arguments.of(GET_CHAIN_DEAL_FAILED, EXECUTION_NOT_AUTHORIZED_GET_CHAIN_DEAL_FAILED), - Arguments.of(INVALID_SIGNATURE, EXECUTION_NOT_AUTHORIZED_INVALID_SIGNATURE) - ); + Arguments.of(INVALID_SIGNATURE, EXECUTION_NOT_AUTHORIZED_INVALID_SIGNATURE)); } @ParameterizedTest @MethodSource("notAuthorizedParams") - void shouldNotGenerateTeeSessionSinceNotAuthorized(AuthorizationError cause, TeeSessionGenerationError consequence) { + void shouldNotGenerateTeeSessionSinceNotAuthorized(AuthorizationError cause, + TeeSessionGenerationError consequence) { final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization .builder() .chainTaskId(TASK_ID) @@ -122,10 +269,11 @@ void shouldNotGenerateTeeSessionSinceNotAuthorized(AuthorizationError cause, Tee when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); - when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.of(cause)); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)) + .thenReturn(Optional.of(cause)); - final ResponseEntity> response = - teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + final ResponseEntity> response = teeController + .generateTeeSession(AUTHORIZATION, workerpoolAuthorization); assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); assertNotEquals(null, response.getBody()); assertNull(response.getBody().getData()); @@ -134,7 +282,7 @@ void shouldNotGenerateTeeSessionSinceNotAuthorized(AuthorizationError cause, Tee } @Test - void shouldNotGenerateTeeSessionSinceEmptySessionId() throws TeeSessionGenerationException { + void shouldNotGenerateTeeSessionSinceEmptyResponse() throws TeeSessionGenerationException { final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization .builder() .chainTaskId(TASK_ID) @@ -144,11 +292,13 @@ void shouldNotGenerateTeeSessionSinceEmptySessionId() throws TeeSessionGeneratio when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); - when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); - when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenReturn(""); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)) + .thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)) + .thenReturn(null); - final ResponseEntity> response = - teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + final ResponseEntity> response = teeController + .generateTeeSession(AUTHORIZATION, workerpoolAuthorization); assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); assertNull(response.getBody()); } @@ -158,11 +308,9 @@ private static Stream exceptionOnSessionIdGeneration() { Arguments.of( new TeeSessionGenerationException( SECURE_SESSION_GENERATION_FAILED, - String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS) - ) - ), - Arguments.of(new RuntimeException()) - ); + String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", + TASK_ID, WORKER_ADDRESS))), + Arguments.of(new RuntimeException())); } /** @@ -172,7 +320,8 @@ private static Stream exceptionOnSessionIdGeneration() { */ @ParameterizedTest @MethodSource("exceptionOnSessionIdGeneration") - void shouldNotGenerateTeeSessionSinceSessionIdGenerationFailed(Exception exception) throws TeeSessionGenerationException { + void shouldNotGenerateTeeSessionSinceSessionIdGenerationFailed(Exception exception) + throws TeeSessionGenerationException { final WorkerpoolAuthorization workerpoolAuthorization = WorkerpoolAuthorization .builder() .chainTaskId(TASK_ID) @@ -182,11 +331,13 @@ void shouldNotGenerateTeeSessionSinceSessionIdGenerationFailed(Exception excepti when(authorizationService.getChallengeForWorker(workerpoolAuthorization)).thenReturn(CHALLENGE); when(authorizationService.isSignedByHimself(CHALLENGE, AUTHORIZATION, WORKER_ADDRESS)).thenReturn(true); - when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)).thenReturn(Optional.empty()); - when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)).thenThrow(exception); + when(authorizationService.isAuthorizedOnExecutionWithDetailedIssue(workerpoolAuthorization, true)) + .thenReturn(Optional.empty()); + when(teeSessionService.generateTeeSession(TASK_ID, Keys.toChecksumAddress(WORKER_ADDRESS), ENCLAVE_CHALLENGE)) + .thenThrow(exception); - final ResponseEntity> response = - teeController.generateTeeSession(AUTHORIZATION, workerpoolAuthorization); + final ResponseEntity> response = teeController + .generateTeeSession(AUTHORIZATION, workerpoolAuthorization); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertNotEquals(null, response.getBody()); assertNull(response.getBody().getData()); diff --git a/src/test/java/com/iexec/sms/tee/TeeFrameworkConverterTests.java b/src/test/java/com/iexec/sms/tee/TeeFrameworkConverterTests.java new file mode 100644 index 00000000..02697da5 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/TeeFrameworkConverterTests.java @@ -0,0 +1,38 @@ +package com.iexec.sms.tee; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static com.iexec.common.tee.TeeFramework.GRAMINE; +import static com.iexec.common.tee.TeeFramework.SCONE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TeeFrameworkConverterTests { + private final TeeFrameworkConverter converter = new TeeFrameworkConverter(); + + // region convert + @Test + void shouldConvertScone() { + assertEquals(SCONE, converter.convert("scone")); + assertEquals(SCONE, converter.convert("Scone")); + assertEquals(SCONE, converter.convert("SCONE")); + assertEquals(SCONE, converter.convert("sCoNe")); + } + + @Test + void shouldConvertGramine() { + assertEquals(GRAMINE, converter.convert("gramine")); + assertEquals(GRAMINE, converter.convert("Gramine")); + assertEquals(GRAMINE, converter.convert("GRAMINE")); + assertEquals(GRAMINE, converter.convert("gRaMiNe")); + } + + @ParameterizedTest + @ValueSource(strings = {"", "azer"}) + void shouldNotConvertBadValue(String value) { + assertThrows(IllegalArgumentException.class, () -> converter.convert(value)); + } + // endregion +} diff --git a/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java b/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java index 6cc471b4..59680917 100644 --- a/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java +++ b/src/test/java/com/iexec/sms/tee/challenge/TeeChallengeServiceTests.java @@ -36,9 +36,9 @@ class TeeChallengeServiceTests { - private final static String TASK_ID = "0x123"; - private final static String PLAIN_PRIVATE = "plainPrivate"; - private final static String ENC_PRIVATE = "encPrivate"; + private static final String TASK_ID = "0x123"; + private static final String PLAIN_PRIVATE = "plainPrivate"; + private static final String ENC_PRIVATE = "encPrivate"; @Mock private TeeChallengeRepository teeChallengeRepository; diff --git a/src/test/java/com/iexec/sms/tee/config/TeeWorkerInternalConfigurationTests.java b/src/test/java/com/iexec/sms/tee/config/TeeWorkerInternalConfigurationTests.java new file mode 100644 index 00000000..28554630 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/config/TeeWorkerInternalConfigurationTests.java @@ -0,0 +1,87 @@ +package com.iexec.sms.tee.config; + +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.config.GramineServicesProperties; +import com.iexec.sms.api.config.SconeServicesProperties; +import com.iexec.sms.api.config.TeeAppProperties; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +class TeeWorkerInternalConfigurationTests { + private final static String IMAGE = "image"; + private final static String FINGERPRINT = "fingerprint"; + private final static String ENTRYPOINT = "entrypoint"; + private final static String LAS_IMAGE = "lasImage"; + private final static long HEAP_SIZE_GB = 3; + private final static long HEAP_SIZE_B = 3221225472L; + + private final TeeWorkerInternalConfiguration teeWorkerInternalConfiguration = + new TeeWorkerInternalConfiguration(); + + // region preComputeProperties + @Test + void preComputeProperties() { + TeeAppProperties properties = teeWorkerInternalConfiguration.preComputeProperties( + IMAGE, + FINGERPRINT, + ENTRYPOINT, + HEAP_SIZE_GB + ); + + assertEquals(IMAGE, properties.getImage()); + assertEquals(FINGERPRINT, properties.getFingerprint()); + assertEquals(ENTRYPOINT, properties.getEntrypoint()); + assertEquals(HEAP_SIZE_B, properties.getHeapSizeInBytes()); + } + // endregion + + // region postComputeProperties + @Test + void postComputeProperties() { + TeeAppProperties properties = teeWorkerInternalConfiguration.postComputeProperties( + IMAGE, + FINGERPRINT, + ENTRYPOINT, + HEAP_SIZE_GB + ); + + assertEquals(IMAGE, properties.getImage()); + assertEquals(FINGERPRINT, properties.getFingerprint()); + assertEquals(ENTRYPOINT, properties.getEntrypoint()); + assertEquals(HEAP_SIZE_B, properties.getHeapSizeInBytes()); + } + // endregion + + // region gramineServicesProperties + @Test + void gramineServicesProperties() { + TeeAppProperties preComputeProperties = mock(TeeAppProperties.class); + TeeAppProperties postComputeProperties = mock(TeeAppProperties.class); + + GramineServicesProperties properties = + teeWorkerInternalConfiguration.gramineServicesProperties(preComputeProperties, postComputeProperties); + + assertEquals(TeeFramework.GRAMINE, properties.getTeeFramework()); + assertEquals(preComputeProperties, properties.getPreComputeProperties()); + assertEquals(postComputeProperties, properties.getPostComputeProperties()); + } + // endregion + + // region sconeServicesProperties + @Test + void sconeServicesProperties() { + TeeAppProperties preComputeProperties = mock(TeeAppProperties.class); + TeeAppProperties postComputeProperties = mock(TeeAppProperties.class); + + SconeServicesProperties properties = + teeWorkerInternalConfiguration.sconeServicesProperties(preComputeProperties, postComputeProperties, LAS_IMAGE); + + assertEquals(TeeFramework.SCONE, properties.getTeeFramework()); + assertEquals(preComputeProperties, properties.getPreComputeProperties()); + assertEquals(postComputeProperties, properties.getPostComputeProperties()); + assertEquals(LAS_IMAGE, properties.getLasImage()); + } + // endregion +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java b/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java index 30225d59..4acc4227 100644 --- a/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java +++ b/src/test/java/com/iexec/sms/tee/session/TeeSessionServiceTests.java @@ -1,87 +1,113 @@ package com.iexec.sms.tee.session; import com.iexec.common.task.TaskDescription; +import com.iexec.common.tee.TeeFramework; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.api.TeeSessionGenerationResponse; import com.iexec.sms.blockchain.IexecHubService; -import com.iexec.sms.tee.session.cas.CasClient; -import com.iexec.sms.tee.session.palaemon.PalaemonSessionService; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.gramine.GramineSessionHandlerService; +import com.iexec.sms.tee.session.scone.SconeSessionHandlerService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.http.ResponseEntity; -import static com.iexec.sms.api.TeeSessionGenerationError.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; class TeeSessionServiceTests { private final static String TASK_ID = "0x0"; private final static String WORKER_ADDRESS = "0x1"; private final static String TEE_CHALLENGE = "0x2"; - + private static final String SECRET_PROVISIONING_URL = "https://secretProvisioningUrl"; @Mock - IexecHubService iexecHubService; - + private SconeSessionHandlerService sconeService; @Mock - CasClient casClient; - + private GramineSessionHandlerService gramineService; @Mock - PalaemonSessionService palaemonSessionService; - - TeeSessionService teeSessionService; + private IexecHubService iexecHubService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - teeSessionService = new TeeSessionService(iexecHubService, palaemonSessionService, casClient, false); } @Test - void shouldGenerateTeeSession() throws TeeSessionGenerationException { - final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); - final String sessionYmlAsString = "YML session"; - + void shouldGenerateSconeSession() + throws TeeSessionGenerationException { + final TeeSessionService teeSessionService = new TeeSessionService(iexecHubService, sconeService); + + final TaskDescription taskDescription = TaskDescription.builder() + .chainTaskId(TASK_ID) + .teeFramework(TeeFramework.SCONE) + .build(); when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); - when(palaemonSessionService.getSessionYml(any())).thenReturn(sessionYmlAsString); - when(casClient.generateSecureSession(sessionYmlAsString.getBytes())).thenReturn(ResponseEntity.ok(null)); - - final String teeSession = assertDoesNotThrow(() -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); - assertNotNull(teeSession); + when(sconeService.buildAndPostSession(any())).thenReturn(SECRET_PROVISIONING_URL); + + final TeeSessionGenerationResponse teeSessionReponse = assertDoesNotThrow( + () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + verify(sconeService, times(1)) + .buildAndPostSession(any()); + assertFalse(teeSessionReponse.getSessionId().isEmpty()); + assertEquals(SECRET_PROVISIONING_URL, teeSessionReponse.getSecretProvisioningUrl()); } @Test - void shouldNotGenerateTeeSessionSinceCantGetTaskDescription() { - when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(null); - - final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); - assertEquals(GET_TASK_DESCRIPTION_FAILED, teeSessionGenerationException.getError()); - assertEquals(String.format("Failed to get task description [taskId:%s]", TASK_ID), teeSessionGenerationException.getMessage()); + void shouldGenerateGramineSession() + throws TeeSessionGenerationException { + final TeeSessionService teeSessionService = new TeeSessionService(iexecHubService, gramineService); + + final TaskDescription taskDescription = TaskDescription.builder() + .chainTaskId(TASK_ID) + .teeFramework(TeeFramework.GRAMINE) + .build(); + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); + when(gramineService.buildAndPostSession(any())).thenReturn(SECRET_PROVISIONING_URL); + + final TeeSessionGenerationResponse teeSessionReponse = assertDoesNotThrow( + () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + verify(gramineService, times(1)) + .buildAndPostSession(any()); + assertFalse(teeSessionReponse.getSessionId().isEmpty()); + assertEquals(SECRET_PROVISIONING_URL, teeSessionReponse.getSecretProvisioningUrl()); } @Test - void shouldNotGenerateTeeSessionSinceCantGetSessionYml() throws TeeSessionGenerationException { - final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); + void shouldNotGenerateTeeSessionSinceCantGetTaskDescription() { + final TeeSessionService teeSessionService = new TeeSessionService(iexecHubService, sconeService); - when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); - when(palaemonSessionService.getSessionYml(any())).thenReturn(""); + when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(null); - final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); - assertEquals(GET_SESSION_YML_FAILED, teeSessionGenerationException.getError()); - assertEquals(String.format("Failed to get session yml [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS), teeSessionGenerationException.getMessage()); + final TeeSessionGenerationException teeSessionGenerationException = assertThrows( + TeeSessionGenerationException.class, + () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertEquals(TeeSessionGenerationError.GET_TASK_DESCRIPTION_FAILED, + teeSessionGenerationException.getError()); + assertEquals(String.format("Failed to get task description [taskId:%s]", TASK_ID), + teeSessionGenerationException.getMessage()); } @Test - void shouldNotGenerateTeeSessionSinceCantGenerateSecureSession() throws TeeSessionGenerationException { - final TaskDescription taskDescription = TaskDescription.builder().chainTaskId(TASK_ID).build(); - final String sessionYmlAsString = "YML session"; + void shouldNotGenerateTeeSessionSinceNoTeeFramework() { + final TeeSessionService teeSessionService = new TeeSessionService(iexecHubService, sconeService); + + final TaskDescription taskDescription = TaskDescription.builder() + .chainTaskId(TASK_ID) + .teeFramework(null) + .build(); when(iexecHubService.getTaskDescription(TASK_ID)).thenReturn(taskDescription); - when(palaemonSessionService.getSessionYml(any())).thenReturn(sessionYmlAsString); - when(casClient.generateSecureSession(sessionYmlAsString.getBytes())).thenReturn(ResponseEntity.notFound().build()); - final TeeSessionGenerationException teeSessionGenerationException = assertThrows(TeeSessionGenerationException.class, () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); - assertEquals(SECURE_SESSION_CAS_CALL_FAILED, teeSessionGenerationException.getError()); - assertEquals(String.format("Failed to generate secure session [taskId:%s, workerAddress:%s]", TASK_ID, WORKER_ADDRESS), teeSessionGenerationException.getMessage()); + final TeeSessionGenerationException teeSessionGenerationException = assertThrows( + TeeSessionGenerationException.class, + () -> teeSessionService.generateTeeSession(TASK_ID, WORKER_ADDRESS, TEE_CHALLENGE)); + assertEquals(TeeSessionGenerationError.SECURE_SESSION_NO_TEE_PROVIDER, + teeSessionGenerationException.getError()); + assertEquals(String.format("TEE framework can't be null [taskId:%s]", + TASK_ID), + teeSessionGenerationException.getMessage()); } + } \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/session/TeeSessionTestUtils.java b/src/test/java/com/iexec/sms/tee/session/TeeSessionTestUtils.java new file mode 100644 index 00000000..ad3cfdf2 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/TeeSessionTestUtils.java @@ -0,0 +1,201 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session; + +import com.iexec.common.precompute.PreComputeUtils; +import com.iexec.common.task.TaskDescription; +import com.iexec.common.tee.TeeEnclaveConfiguration; +import com.iexec.common.utils.IexecEnvUtils; +import com.iexec.sms.secret.compute.OnChainObjectType; +import com.iexec.sms.secret.compute.SecretOwnerRole; +import com.iexec.sms.secret.compute.TeeTaskComputeSecret; +import com.iexec.sms.tee.session.base.SecretSessionBaseService; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ClassUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.iexec.common.worker.result.ResultUtils.*; +import static com.iexec.sms.Web3jUtils.createEthereumAddress; +import static com.iexec.sms.tee.session.base.SecretSessionBaseService.*; +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +public class TeeSessionTestUtils { + public static final String TASK_ID = "taskId"; + public static final String SESSION_ID = "sessionId"; + public static final String WORKER_ADDRESS = "workerAddress"; + public static final String ENCLAVE_CHALLENGE = "enclaveChallenge"; + // pre-compute + public static final String PRE_COMPUTE_FINGERPRINT = "mrEnclave1"; + public static final String PRE_COMPUTE_ENTRYPOINT = "entrypoint1"; + public static final String DATASET_ADDRESS = "0xDatasetAddress"; + public static final String DATASET_NAME = "datasetName"; + public static final String DATASET_CHECKSUM = "datasetChecksum"; + public static final String DATASET_URL = "http://datasetUrl"; // 0x687474703a2f2f646174617365742d75726c in hex + // keys with leading/trailing \n should not break the workflow + public static final String DATASET_KEY = "\ndatasetKey\n"; + // app + public static final String APP_DEVELOPER_SECRET_INDEX = "1"; + public static final String APP_DEVELOPER_SECRET_VALUE = "appDeveloperSecretValue"; + public static final String REQUESTER_SECRET_KEY_1 = "requesterSecretKey1"; + public static final String REQUESTER_SECRET_VALUE_1 = "requesterSecretValue1"; + public static final String REQUESTER_SECRET_KEY_2 = "requesterSecretKey2"; + public static final String REQUESTER_SECRET_VALUE_2 = "requesterSecretValue2"; + public static final String APP_URI = "appUri"; + public static final String APP_FINGERPRINT = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"; + public static final String APP_ENTRYPOINT = "appEntrypoint"; + public static final String ARGS = "args"; + public static final String IEXEC_APP_DEVELOPER_SECRET_1 = "IEXEC_APP_DEVELOPER_SECRET_1"; + // post-compute + public static final String POST_COMPUTE_FINGERPRINT = "mrEnclave3"; + public static final String POST_COMPUTE_ENTRYPOINT = "entrypoint3"; + public static final String STORAGE_PROVIDER = "ipfs"; + public static final String STORAGE_PROXY = "storageProxy"; + public static final String STORAGE_TOKEN = "storageToken"; + public static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey"; + public static final String TEE_CHALLENGE_PRIVATE_KEY = "teeChallengePrivateKey"; + // input files + public static final String INPUT_FILE_URL_1 = "http://host/file1"; + public static final String INPUT_FILE_NAME_1 = "file1"; + public static final String INPUT_FILE_URL_2 = "http://host/file2"; + public static final String INPUT_FILE_NAME_2 = "file2"; + + //region utils + public static TeeTaskComputeSecret getApplicationDeveloperSecret(String appAddress) { + return TeeTaskComputeSecret.builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress(appAddress) + .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) + .fixedSecretOwner("") + .key(APP_DEVELOPER_SECRET_INDEX) + .value(APP_DEVELOPER_SECRET_VALUE) + .build(); + } + + public static TeeTaskComputeSecret getRequesterSecret(String requesterAddress, String secretKey, String secretValue) { + return TeeTaskComputeSecret.builder() + .onChainObjectType(OnChainObjectType.APPLICATION) + .onChainObjectAddress("") + .secretOwnerRole(SecretOwnerRole.REQUESTER) + .fixedSecretOwner(requesterAddress) + .key(secretKey) + .value(secretValue) + .build(); + } + + public static TeeSessionRequest createSessionRequest(TaskDescription taskDescription) { + return TeeSessionRequest.builder() + .sessionId(SESSION_ID) + .workerAddress(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .taskDescription(taskDescription) + .build(); + } + + public static TaskDescription createTaskDescription(TeeEnclaveConfiguration enclaveConfig) { + String appAddress = createEthereumAddress(); + String requesterAddress = createEthereumAddress(); + String beneficiaryAddress = createEthereumAddress(); + return TaskDescription.builder() + .chainTaskId(TASK_ID) + .appUri(APP_URI) + .appAddress(appAddress) + .appEnclaveConfiguration(enclaveConfig) + .datasetAddress(DATASET_ADDRESS) + .datasetUri(DATASET_URL) + .datasetName(DATASET_NAME) + .datasetChecksum(DATASET_CHECKSUM) + .requester(requesterAddress) + .beneficiary(beneficiaryAddress) + .cmd(ARGS) + .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) + .isResultEncryption(true) + .resultStorageProvider(STORAGE_PROVIDER) + .resultStorageProxy(STORAGE_PROXY) + .secrets(Map.of("1", REQUESTER_SECRET_KEY_1, "2", REQUESTER_SECRET_KEY_2)) + .botSize(1) + .botFirstIndex(0) + .botIndex(0) + .build(); + } + + public static Map getPreComputeTokens() { + return Map.of( + PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, + PreComputeUtils.IS_DATASET_REQUIRED, true, + PreComputeUtils.IEXEC_DATASET_KEY, DATASET_KEY.trim(), + INPUT_FILE_URLS, Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, + IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2)); + } + + public static Map getAppTokens() { + return Map.of( + APP_MRENCLAVE, APP_FINGERPRINT, + SecretSessionBaseService.INPUT_FILE_NAMES, Map.of( + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", INPUT_FILE_NAME_1, + IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", INPUT_FILE_NAME_2)); + } + + public static Map getPostComputeTokens() { + Map map = new HashMap<>(); + map.put(POST_COMPUTE_MRENCLAVE, POST_COMPUTE_FINGERPRINT); + map.put(RESULT_TASK_ID, TASK_ID); + map.put(RESULT_ENCRYPTION, "yes"); + map.put(RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY); + map.put(RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER); + map.put(RESULT_STORAGE_PROXY, STORAGE_PROXY); + map.put(RESULT_STORAGE_TOKEN, STORAGE_TOKEN); + map.put(RESULT_STORAGE_CALLBACK, "no"); + map.put(RESULT_SIGN_WORKER_ADDRESS, WORKER_ADDRESS); + map.put(RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, TEE_CHALLENGE_PRIVATE_KEY); + return map; + } + + public static void assertRecursively(Object expected, Object actual) { + if (expected == null || + expected instanceof String || + ClassUtils.isPrimitiveOrWrapper(expected.getClass())) { + log.info("Comparing [actual:{}, expected:{}]", expected, actual); + assertThat(expected).isEqualTo(actual); + return; + } + if (expected instanceof List) { + List actualList = (List) expected; + List expectedList = (List) actual; + for (int i = 0; i < actualList.size(); i++) { + assertRecursively(actualList.get(i), expectedList.get(i)); + } + return; + } + if (expected instanceof Map) { + Map actualMap = (Map) expected; + Map expectedMap = (Map) actual; + actualMap.keySet().forEach((key) -> { + final Object expectedObject = expectedMap.get(key); + final Object actualObject = actualMap.get(key); + log.info("Checking expected map contains valid '{}' key [expected value:{}, actual value:{}]", key, expectedObject, actualObject); + assertRecursively(expectedObject, actualObject); + }); + } + } + //endregion +} diff --git a/src/test/java/com/iexec/sms/tee/session/base/SecretSessionBaseServiceTests.java b/src/test/java/com/iexec/sms/tee/session/base/SecretSessionBaseServiceTests.java new file mode 100644 index 00000000..9030e295 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/base/SecretSessionBaseServiceTests.java @@ -0,0 +1,693 @@ +package com.iexec.sms.tee.session.base; + +import com.iexec.common.chain.DealParams; +import com.iexec.common.task.TaskDescription; +import com.iexec.common.tee.TeeEnclaveConfiguration; +import com.iexec.common.tee.TeeFramework; +import com.iexec.common.utils.IexecEnvUtils; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.api.config.TeeServicesProperties; +import com.iexec.sms.secret.ReservedSecretKeyName; +import com.iexec.sms.secret.compute.OnChainObjectType; +import com.iexec.sms.secret.compute.SecretOwnerRole; +import com.iexec.sms.secret.compute.TeeTaskComputeSecret; +import com.iexec.sms.secret.compute.TeeTaskComputeSecretService; +import com.iexec.sms.secret.web2.Web2SecretService; +import com.iexec.sms.secret.web3.Web3SecretService; +import com.iexec.sms.tee.challenge.TeeChallenge; +import com.iexec.sms.tee.challenge.TeeChallengeService; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.utils.EthereumCredentials; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.iexec.sms.tee.session.TeeSessionTestUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class SecretSessionBaseServiceTests { + + private static final TeeEnclaveConfiguration enclaveConfig = TeeEnclaveConfiguration.builder() + .framework(TeeFramework.SCONE)// any would be fine + .entrypoint(APP_ENTRYPOINT) + .fingerprint(APP_FINGERPRINT) + .heapSize(1) + .build(); + + @Mock + private Web3SecretService web3SecretService; + @Mock + private Web2SecretService web2SecretService; + @Mock + private TeeChallengeService teeChallengeService; + @Mock + private TeeAppProperties preComputeProperties; + @Mock + private TeeAppProperties postComputeProperties; + @Mock + private TeeServicesProperties teeServicesConfig; + @Mock + private TeeTaskComputeSecretService teeTaskComputeSecretService; + + @InjectMocks + private SecretSessionBaseService teeSecretsService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + when(teeServicesConfig.getPreComputeProperties()).thenReturn(preComputeProperties); + when(teeServicesConfig.getPostComputeProperties()).thenReturn(postComputeProperties); + } + + // region getSecretsTokens + @Test + void shouldGetSecretsTokens() throws Exception { + TaskDescription taskDescription = createTaskDescription(enclaveConfig); + TeeSessionRequest request = createSessionRequest(taskDescription); + String beneficiary = request.getTaskDescription().getBeneficiary(); + + // pre + when(preComputeProperties.getFingerprint()) + .thenReturn(PRE_COMPUTE_FINGERPRINT); + when(web3SecretService.getDecryptedValue(DATASET_ADDRESS)) + .thenReturn(Optional.of(DATASET_KEY)); + // post + when(postComputeProperties.getFingerprint()) + .thenReturn(POST_COMPUTE_FINGERPRINT); + when(web2SecretService.getDecryptedValue( + beneficiary, + ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY)) + .thenReturn(Optional.of(ENCRYPTION_PUBLIC_KEY)); + when(web2SecretService.getDecryptedValue(taskDescription.getRequester(), + ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN)) + .thenReturn(Optional.of(STORAGE_TOKEN)); + TeeChallenge challenge = TeeChallenge.builder() + .credentials(EthereumCredentials.generate()) + .build(); + when(teeChallengeService.getOrCreate(TASK_ID, true)) + .thenReturn(Optional.of(challenge)); + + SecretSessionBase sessionBase = teeSecretsService.getSecretsTokens(request); + + SecretEnclaveBase preComputeBase = sessionBase.getPreCompute(); + assertEquals("pre-compute", preComputeBase.getName()); + assertEquals(PRE_COMPUTE_FINGERPRINT, preComputeBase.getMrenclave()); + // environment content checks are handled in dedicated tests below + assertEquals(teeSecretsService.getPreComputeTokens(request).getEnvironment(), + preComputeBase.getEnvironment()); + + SecretEnclaveBase appComputeBase = sessionBase.getAppCompute(); + assertEquals("app", appComputeBase.getName()); + assertEquals(APP_FINGERPRINT, appComputeBase.getMrenclave()); + // environment content checks are handled in dedicated tests below + assertEquals(teeSecretsService.getAppTokens(request).getEnvironment(), + appComputeBase.getEnvironment()); + + SecretEnclaveBase postComputeBase = sessionBase.getPostCompute(); + assertEquals("post-compute", postComputeBase.getName()); + assertEquals(POST_COMPUTE_FINGERPRINT, postComputeBase.getMrenclave()); + // environment content checks are handled in dedicated tests below + assertEquals(teeSecretsService.getPostComputeTokens(request).getEnvironment(), + postComputeBase.getEnvironment()); + } + + @Test + void shouldNotGetSecretsTokensSinceRequestIsNull() { + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getSecretsTokens(null)); + assertEquals(TeeSessionGenerationError.NO_SESSION_REQUEST, exception.getError()); + assertEquals("Session request must not be null", exception.getMessage()); + } + + @Test + void shouldNotGetSecretsTokensSinceTaskDescriptionIsMissing() { + TeeSessionRequest request = TeeSessionRequest.builder().build(); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getSecretsTokens(request)); + assertEquals(TeeSessionGenerationError.NO_TASK_DESCRIPTION, exception.getError()); + assertEquals("Task description must not be null", exception.getMessage()); + } + // endregion + + // region getPreComputeTokens + @Test + void shouldGetPreComputeTokens() throws Exception { + TaskDescription taskDescription = createTaskDescription(enclaveConfig); + TeeSessionRequest request = createSessionRequest(taskDescription); + when(preComputeProperties.getFingerprint()) + .thenReturn(PRE_COMPUTE_FINGERPRINT); + when(web3SecretService.getDecryptedValue(DATASET_ADDRESS)) + .thenReturn(Optional.of(DATASET_KEY)); + + SecretEnclaveBase enclaveBase = teeSecretsService.getPreComputeTokens(request); + assertThat(enclaveBase.getName()).isEqualTo("pre-compute"); + assertThat(enclaveBase.getMrenclave()).isEqualTo(PRE_COMPUTE_FINGERPRINT); + Map expectedTokens = new HashMap<>(); + expectedTokens.put("IEXEC_TASK_ID", TASK_ID); + expectedTokens.put("IEXEC_PRE_COMPUTE_OUT", "/iexec_in"); + expectedTokens.put("IS_DATASET_REQUIRED", true); + expectedTokens.put("IEXEC_DATASET_KEY", DATASET_KEY); + expectedTokens.put("IEXEC_DATASET_URL", DATASET_URL); + expectedTokens.put("IEXEC_DATASET_FILENAME", DATASET_NAME); + expectedTokens.put("IEXEC_DATASET_CHECKSUM", DATASET_CHECKSUM); + expectedTokens.put("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"); + expectedTokens.put("IEXEC_INPUT_FILES_NUMBER", "2"); + expectedTokens.put("IEXEC_INPUT_FILE_URL_1", INPUT_FILE_URL_1); + expectedTokens.put("IEXEC_INPUT_FILE_URL_2", INPUT_FILE_URL_2); + assertThat(enclaveBase.getEnvironment()).containsExactlyInAnyOrderEntriesOf(expectedTokens); + } + + @Test + void shouldGetPreComputeTokensWithoutDataset() throws Exception { + TeeSessionRequest request = TeeSessionRequest.builder() + .sessionId(SESSION_ID) + .workerAddress(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .taskDescription(TaskDescription.builder() + .chainTaskId(TASK_ID) + .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) + .build()) + .build(); + when(preComputeProperties.getFingerprint()) + .thenReturn(PRE_COMPUTE_FINGERPRINT); + + SecretEnclaveBase enclaveBase = teeSecretsService.getPreComputeTokens(request); + assertThat(enclaveBase.getName()).isEqualTo("pre-compute"); + assertThat(enclaveBase.getMrenclave()).isEqualTo(PRE_COMPUTE_FINGERPRINT); + Map expectedTokens = new HashMap<>(); + expectedTokens.put("IEXEC_TASK_ID", TASK_ID); + expectedTokens.put("IEXEC_PRE_COMPUTE_OUT", "/iexec_in"); + expectedTokens.put("IS_DATASET_REQUIRED", false); + expectedTokens.put("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"); + expectedTokens.put("IEXEC_INPUT_FILES_NUMBER", "2"); + expectedTokens.put("IEXEC_INPUT_FILE_URL_1", INPUT_FILE_URL_1); + expectedTokens.put("IEXEC_INPUT_FILE_URL_2", INPUT_FILE_URL_2); + assertThat(enclaveBase.getEnvironment()).containsExactlyInAnyOrderEntriesOf(expectedTokens); + } + // endregion + + // region getAppTokens + @Test + void shouldGetAppTokensForAdvancedTaskDescription() throws TeeSessionGenerationException { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + String appAddress = request.getTaskDescription().getAppAddress(); + String requesterAddress = request.getTaskDescription().getRequester(); + + addApplicationDeveloperSecret(appAddress); + addRequesterSecret(requesterAddress, REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + addRequesterSecret(requesterAddress, REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); + + SecretEnclaveBase enclaveBase = teeSecretsService.getAppTokens(request); + assertThat(enclaveBase.getName()).isEqualTo("app"); + assertThat(enclaveBase.getMrenclave()).isEqualTo(APP_FINGERPRINT); + Map expectedTokens = new HashMap<>(); + expectedTokens.put("IEXEC_TASK_ID", TASK_ID); + expectedTokens.put("IEXEC_IN", "/iexec_in"); + expectedTokens.put("IEXEC_OUT", "/iexec_out"); + expectedTokens.put("IEXEC_DATASET_ADDRESS", DATASET_ADDRESS); + expectedTokens.put("IEXEC_DATASET_FILENAME", DATASET_NAME); + expectedTokens.put("IEXEC_BOT_SIZE", "1"); + expectedTokens.put("IEXEC_BOT_FIRST_INDEX", "0"); + expectedTokens.put("IEXEC_BOT_TASK_INDEX", "0"); + expectedTokens.put("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"); + expectedTokens.put("IEXEC_INPUT_FILES_NUMBER", "2"); + expectedTokens.put("IEXEC_INPUT_FILE_NAME_1", INPUT_FILE_NAME_1); + expectedTokens.put("IEXEC_INPUT_FILE_NAME_2", INPUT_FILE_NAME_2); + expectedTokens.put("IEXEC_APP_DEVELOPER_SECRET", APP_DEVELOPER_SECRET_VALUE); + expectedTokens.put("IEXEC_APP_DEVELOPER_SECRET_1", APP_DEVELOPER_SECRET_VALUE); + expectedTokens.put("IEXEC_REQUESTER_SECRET_1", REQUESTER_SECRET_VALUE_1); + expectedTokens.put("IEXEC_REQUESTER_SECRET_2", REQUESTER_SECRET_VALUE_2); + assertThat(enclaveBase.getEnvironment()).containsExactlyInAnyOrderEntriesOf(expectedTokens); + + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, + requesterAddress, REQUESTER_SECRET_KEY_1); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, + requesterAddress, REQUESTER_SECRET_KEY_2); + } + + @Test + void shouldGetTokensWithEmptyAppComputeSecretWhenSecretsDoNotExist() throws TeeSessionGenerationException { + final String appAddress = "0xapp"; + final String requesterAddress = "0xrequester"; + final TaskDescription taskDescription = TaskDescription.builder() + .chainTaskId(TASK_ID) + .appUri(APP_URI) + .appAddress(appAddress) + .appEnclaveConfiguration(enclaveConfig) + .datasetAddress(DATASET_ADDRESS) + .datasetUri(DATASET_URL) + .datasetName(DATASET_NAME) + .datasetChecksum(DATASET_CHECKSUM) + .requester(requesterAddress) + .cmd(ARGS) + .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) + .isResultEncryption(true) + .resultStorageProvider(STORAGE_PROVIDER) + .resultStorageProxy(STORAGE_PROXY) + .botSize(1) + .botFirstIndex(0) + .botIndex(0) + .build(); + TeeSessionRequest request = createSessionRequest(taskDescription); + + when(teeTaskComputeSecretService.getSecret( + OnChainObjectType.APPLICATION, + appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, + "", + APP_DEVELOPER_SECRET_INDEX)) + .thenReturn(Optional.empty()); + + SecretEnclaveBase enclaveBase = teeSecretsService.getAppTokens(request); + assertThat(enclaveBase.getName()).isEqualTo("app"); + assertThat(enclaveBase.getMrenclave()).isEqualTo(APP_FINGERPRINT); + Map expectedTokens = new HashMap<>(); + expectedTokens.put("IEXEC_TASK_ID", TASK_ID); + expectedTokens.put("IEXEC_IN", "/iexec_in"); + expectedTokens.put("IEXEC_OUT", "/iexec_out"); + expectedTokens.put("IEXEC_DATASET_ADDRESS", DATASET_ADDRESS); + expectedTokens.put("IEXEC_DATASET_FILENAME", DATASET_NAME); + expectedTokens.put("IEXEC_BOT_SIZE", "1"); + expectedTokens.put("IEXEC_BOT_FIRST_INDEX", "0"); + expectedTokens.put("IEXEC_BOT_TASK_INDEX", "0"); + expectedTokens.put("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"); + expectedTokens.put("IEXEC_INPUT_FILES_NUMBER", "2"); + expectedTokens.put("IEXEC_INPUT_FILE_NAME_1", INPUT_FILE_NAME_1); + expectedTokens.put("IEXEC_INPUT_FILE_NAME_2", INPUT_FILE_NAME_2); + assertThat(enclaveBase.getEnvironment()).containsExactlyInAnyOrderEntriesOf(expectedTokens); + + verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(appAddress), + eq(SecretOwnerRole.APPLICATION_DEVELOPER), eq(""), any()); + verify(teeTaskComputeSecretService, never()).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), + eq(SecretOwnerRole.REQUESTER), any(), any()); + } + + @Test + void shouldFailToGetAppTokensSinceNoTaskDescription() { + TeeSessionRequest request = TeeSessionRequest.builder() + .build(); + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, + () -> teeSecretsService.getAppTokens(request)); + Assertions.assertEquals(TeeSessionGenerationError.NO_TASK_DESCRIPTION, exception.getError()); + Assertions.assertEquals("Task description must not be null", exception.getMessage()); + } + + @Test + void shouldFailToGetAppTokensSinceNoEnclaveConfig() { + TeeSessionRequest request = TeeSessionRequest.builder() + .sessionId(SESSION_ID) + .workerAddress(WORKER_ADDRESS) + .enclaveChallenge(ENCLAVE_CHALLENGE) + .taskDescription(TaskDescription.builder().build()) + .build(); + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, + () -> teeSecretsService.getAppTokens(request)); + Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_NO_ENCLAVE_CONFIG, exception.getError()); + Assertions.assertEquals("Enclave configuration must not be null", exception.getMessage()); + } + + @Test + void shouldFailToGetAppTokensInvalidEnclaveConfig() { + // invalid enclave config + TeeSessionRequest request = createSessionRequest(createTaskDescription(TeeEnclaveConfiguration.builder().build())); + + TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, + () -> teeSecretsService.getAppTokens(request)); + Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_INVALID_ENCLAVE_CONFIG, exception.getError()); + } + + @Test + void shouldAddMultipleRequesterSecrets() { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + String requesterAddress = request.getTaskDescription().getRequester(); + + addRequesterSecret(requesterAddress, REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + addRequesterSecret(requesterAddress, REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); + SecretEnclaveBase enclaveBase = assertDoesNotThrow(() -> teeSecretsService.getAppTokens(request)); + verify(teeTaskComputeSecretService, times(2)) + .getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, + requesterAddress, REQUESTER_SECRET_KEY_1); + verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, + requesterAddress, REQUESTER_SECRET_KEY_2); + assertThat(enclaveBase.getEnvironment()).containsAllEntriesOf( + Map.of( + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1, + IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "2", REQUESTER_SECRET_VALUE_2)); + } + + @Test + void shouldFilterRequesterSecretIndexLowerThanZero() { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + String requesterAddress = request.getTaskDescription().getRequester(); + + request.getTaskDescription() + .setSecrets(Map.of("1", REQUESTER_SECRET_KEY_1, "-1", "out-of-bound-requester-secret")); + addRequesterSecret(requesterAddress, REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); + SecretEnclaveBase enclaveBase = assertDoesNotThrow(() -> teeSecretsService.getAppTokens(request)); + verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), + eq(SecretOwnerRole.REQUESTER), any(), any()); + assertThat(enclaveBase.getEnvironment()).containsAllEntriesOf( + Map.of(IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1)); + } + // endregion + + // region getPostComputeTokens + @Test + void shouldGetPostComputeTokens() throws Exception { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + String requesterAddress = request.getTaskDescription().getRequester(); + + when(postComputeProperties.getFingerprint()) + .thenReturn(POST_COMPUTE_FINGERPRINT); + when(web2SecretService.getDecryptedValue( + request.getTaskDescription().getBeneficiary(), + ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY)) + .thenReturn(Optional.of(ENCRYPTION_PUBLIC_KEY)); + when(web2SecretService.getDecryptedValue( + requesterAddress, + ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN)) + .thenReturn(Optional.of(STORAGE_TOKEN)); + + TeeChallenge challenge = TeeChallenge.builder() + .credentials(EthereumCredentials.generate()) + .build(); + when(teeChallengeService.getOrCreate(TASK_ID, true)) + .thenReturn(Optional.of(challenge)); + + SecretEnclaveBase enclaveBase = teeSecretsService.getPostComputeTokens(request); + assertThat(enclaveBase.getName()).isEqualTo("post-compute"); + assertThat(enclaveBase.getMrenclave()).isEqualTo(POST_COMPUTE_FINGERPRINT); + Map expectedTokens = new HashMap<>(); + // encryption tokens + expectedTokens.put("RESULT_ENCRYPTION", "yes"); + expectedTokens.put("RESULT_ENCRYPTION_PUBLIC_KEY", ENCRYPTION_PUBLIC_KEY); + // storage tokens + expectedTokens.put("RESULT_STORAGE_CALLBACK", "no"); + expectedTokens.put("RESULT_STORAGE_PROVIDER", STORAGE_PROVIDER); + expectedTokens.put("RESULT_STORAGE_PROXY", STORAGE_PROXY); + expectedTokens.put("RESULT_STORAGE_TOKEN", STORAGE_TOKEN); + // sign tokens + expectedTokens.put("RESULT_TASK_ID", TASK_ID); + expectedTokens.put("RESULT_SIGN_WORKER_ADDRESS", WORKER_ADDRESS); + expectedTokens.put("RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY", challenge.getCredentials().getPrivateKey()); + + assertThat(enclaveBase.getEnvironment()).containsExactlyEntriesOf(expectedTokens); + } + + @Test + void shouldNotGetPostComputeTokensSinceTaskDescriptionMissing() { + TeeSessionRequest request = TeeSessionRequest.builder().build(); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeTokens(request)); + assertEquals(TeeSessionGenerationError.NO_TASK_DESCRIPTION, exception.getError()); + assertEquals("Task description must not be null", exception.getMessage()); + } + // endregion + + // region getPostComputeEncryptionTokens + @Test + void shouldGetPostComputeStorageTokensWithCallback() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + sessionRequest.getTaskDescription().setCallback("callback"); + + final Map tokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_STORAGE_CALLBACK", "yes", + "RESULT_STORAGE_PROVIDER", "", + "RESULT_STORAGE_PROXY", "", + "RESULT_STORAGE_TOKEN", "")); + } + + @Test + void shouldGetPostComputeStorageTokensOnIpfs() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + + final String secretValue = "Secret value"; + when(web2SecretService.getDecryptedValue(taskDescription.getRequester(), + ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN)) + .thenReturn(Optional.of(secretValue)); + + final Map tokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_STORAGE_CALLBACK", "no", + "RESULT_STORAGE_PROVIDER", STORAGE_PROVIDER, + "RESULT_STORAGE_PROXY", STORAGE_PROXY, + "RESULT_STORAGE_TOKEN", secretValue)); + } + + @Test + void shouldGetPostComputeStorageTokensOnDropbox() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + taskDescription.setResultStorageProvider(DealParams.DROPBOX_RESULT_STORAGE_PROVIDER); + + final String secretValue = "Secret value"; + when(web2SecretService.getDecryptedValue(taskDescription.getRequester(), + ReservedSecretKeyName.IEXEC_RESULT_DROPBOX_TOKEN)) + .thenReturn(Optional.of(secretValue)); + + final Map tokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_STORAGE_CALLBACK", "no", + "RESULT_STORAGE_PROVIDER", "dropbox", + "RESULT_STORAGE_PROXY", STORAGE_PROXY, + "RESULT_STORAGE_TOKEN", secretValue)); + } + + @Test + void shouldNotGetPostComputeStorageTokensSinceNoSecret() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + + when(web2SecretService.getDecryptedValue(taskDescription.getRequester(), + ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeStorageTokens(sessionRequest)); + + assertThat(exception.getError()).isEqualTo(TeeSessionGenerationError.POST_COMPUTE_GET_STORAGE_TOKENS_FAILED); + assertThat(exception.getMessage()) + .isEqualTo("Empty requester storage token - taskId: " + taskDescription.getChainTaskId()); + } + + @Test + void shouldGetPostComputeSignTokens() throws GeneralSecurityException { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final TaskDescription taskDescription = sessionRequest.getTaskDescription(); + final String taskId = taskDescription.getChainTaskId(); + final EthereumCredentials credentials = EthereumCredentials.generate(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.of(TeeChallenge.builder().credentials(credentials).build())); + + final Map tokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(tokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_TASK_ID", taskId, + "RESULT_SIGN_WORKER_ADDRESS", sessionRequest.getWorkerAddress(), + "RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY", credentials.getPrivateKey())); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = { "" }) + void shouldNotGetPostComputeSignTokensSinceNoWorkerAddress(String emptyWorkerAddress) { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + sessionRequest.setWorkerAddress(emptyWorkerAddress); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(exception.getError()) + .isEqualTo(TeeSessionGenerationError.POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS); + assertThat(exception.getMessage()).isEqualTo("Empty worker address - taskId: " + taskId); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = { "" }) + void shouldNotGetPostComputeSignTokensSinceNoEnclaveChallenge(String emptyEnclaveChallenge) { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + sessionRequest.setEnclaveChallenge(emptyEnclaveChallenge); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(exception.getError()).isEqualTo( + TeeSessionGenerationError.POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE); + assertThat(exception.getMessage()).isEqualTo("Empty public enclave challenge - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoTeeChallenge() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(exception.getError()) + .isEqualTo(TeeSessionGenerationError.POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentials() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional.of(TeeChallenge.builder().credentials(null).build())); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(exception.getError()) + .isEqualTo(TeeSessionGenerationError.POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); + } + + @Test + void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentialsPrivateKey() { + final TeeSessionRequest sessionRequest = createSessionRequest(createTaskDescription(enclaveConfig)); + final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); + + when(teeChallengeService.getOrCreate(taskId, true)) + .thenReturn(Optional + .of(TeeChallenge.builder().credentials(new EthereumCredentials("", "", false, "")).build())); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeSignTokens(sessionRequest)); + + assertThat(exception.getError()) + .isEqualTo(TeeSessionGenerationError.POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); + assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); + } + + @Test + void shouldGetPostComputeEncryptionTokensWithEncryption() { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + String beneficiary = request.getTaskDescription().getBeneficiary(); + when(web2SecretService.getDecryptedValue( + beneficiary, + ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY)) + .thenReturn(Optional.of(ENCRYPTION_PUBLIC_KEY)); + + final Map encryptionTokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeEncryptionTokens(request)); + assertThat(encryptionTokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_ENCRYPTION", "yes", + "RESULT_ENCRYPTION_PUBLIC_KEY", ENCRYPTION_PUBLIC_KEY)); + } + + @Test + void shouldGetPostComputeEncryptionTokensWithoutEncryption() { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + request.getTaskDescription().setResultEncryption(false); + + final Map encryptionTokens = assertDoesNotThrow( + () -> teeSecretsService.getPostComputeEncryptionTokens(request)); + assertThat(encryptionTokens) + .containsExactlyInAnyOrderEntriesOf( + Map.of( + "RESULT_ENCRYPTION", "no", + "RESULT_ENCRYPTION_PUBLIC_KEY", "")); + } + + @Test + void shouldNotGetPostComputeEncryptionTokensSinceEmptyBeneficiaryKey() { + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + when(web2SecretService.getDecryptedValue( + request.getTaskDescription().getBeneficiary(), + ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY)) + .thenReturn(Optional.empty()); + + final TeeSessionGenerationException exception = assertThrows( + TeeSessionGenerationException.class, + () -> teeSecretsService.getPostComputeEncryptionTokens(request)); + assertEquals(TeeSessionGenerationError.POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, + exception.getError()); + assertEquals("Empty beneficiary encryption key - taskId: taskId", exception.getMessage()); + } + + // endregion + + // region utils + private void addApplicationDeveloperSecret(String appAddress) { + TeeTaskComputeSecret applicationDeveloperSecret = getApplicationDeveloperSecret(appAddress); + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, appAddress, + SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX)) + .thenReturn(Optional.of(applicationDeveloperSecret)); + } + + private void addRequesterSecret(String requesterAddress, String secretKey, String secretValue) { + TeeTaskComputeSecret requesterSecret = getRequesterSecret(requesterAddress, secretKey, secretValue); + when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, + requesterAddress, secretKey)) + .thenReturn(Optional.of(requesterSecret)); + } + // endregion + +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerServiceTests.java b/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerServiceTests.java new file mode 100644 index 00000000..6d9a8a9f --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionHandlerServiceTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2022 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.gramine; + +import com.iexec.common.task.TaskDescription; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.tee.session.TeeSessionLogConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.gramine.sps.GramineSession; +import com.iexec.sms.tee.session.gramine.sps.SpsApiClient; +import com.iexec.sms.tee.session.gramine.sps.SpsConfiguration; +import feign.FeignException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(OutputCaptureExtension.class) +class GramineSessionHandlerServiceTests { + + private static final String SPS_URL = "spsUrl"; + @Mock + private GramineSessionMakerService sessionService; + @Mock + private SpsConfiguration spsConfiguration; + @Mock + private TeeSessionLogConfiguration teeSessionLogConfiguration; + @InjectMocks + private GramineSessionHandlerService sessionHandlerService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + when(spsConfiguration.getEnclaveHost()).thenReturn(SPS_URL); + } + + @Test + void shouldBuildAndPostSession(CapturedOutput output) throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + GramineSession spsSession = mock(GramineSession.class); + when(spsSession.toString()).thenReturn("sessionContent"); + when(sessionService.generateSession(request)).thenReturn(spsSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()).thenReturn(true); + SpsApiClient spsClient = mock(SpsApiClient.class); + when(spsClient.postSession(spsSession)).thenReturn("sessionId"); + when(spsConfiguration.getInstance()).thenReturn(spsClient); + + assertEquals(SPS_URL, sessionHandlerService.buildAndPostSession(request)); + // Testing output here since it reflects a business feature (ability to catch a + // session in debug mode) + assertTrue(output.getOut().contains("Session content [taskId:null]\nsessionContent\n")); + } + + @Test + void shouldNotBuildAndPostSessionSinceBuildSessionFailed() + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TeeSessionGenerationException teeSessionGenerationException = new TeeSessionGenerationException( + TeeSessionGenerationError.SECURE_SESSION_GENERATION_FAILED, "some error"); + when(sessionService.generateSession(request)).thenThrow(teeSessionGenerationException); + + assertThrows(teeSessionGenerationException.getClass(), + () -> sessionHandlerService.buildAndPostSession(request)); + } + + @Test + void shouldNotBuildAndPostSessionSincePostSessionFailed() + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + GramineSession spsSession = mock(GramineSession.class); + when(spsSession.toString()).thenReturn("sessionContent"); + when(sessionService.generateSession(request)).thenReturn(spsSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()).thenReturn(true); + SpsApiClient spsClient = mock(SpsApiClient.class); + when(spsConfiguration.getInstance()).thenReturn(spsClient); + FeignException apiClientException = mock(FeignException.class); + when(spsClient.postSession(spsSession)).thenThrow(apiClientException); + + assertThrows(TeeSessionGenerationException.class, + () -> sessionHandlerService.buildAndPostSession(request)); + } + +} diff --git a/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerServiceTests.java b/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerServiceTests.java new file mode 100644 index 00000000..9fedc989 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/gramine/GramineSessionMakerServiceTests.java @@ -0,0 +1,102 @@ +package com.iexec.sms.tee.session.gramine; + +import com.iexec.common.tee.TeeEnclaveConfiguration; +import com.iexec.common.utils.FileHelper; +import com.iexec.sms.api.config.GramineServicesProperties; +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.tee.session.base.SecretEnclaveBase; +import com.iexec.sms.tee.session.base.SecretSessionBase; +import com.iexec.sms.tee.session.base.SecretSessionBaseService; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.gramine.sps.GramineSession; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testcontainers.shaded.org.yaml.snakeyaml.Yaml; + +import java.util.Map; + +import static com.iexec.sms.tee.session.TeeSessionTestUtils.*; +import static org.mockito.Mockito.when; + +@Slf4j +class GramineSessionMakerServiceTests { + @Mock + private TeeAppProperties preComputeProperties; + @Mock + private TeeAppProperties postComputeProperties; + @Mock + private GramineServicesProperties teeServicesConfig; + @Mock + private SecretSessionBaseService teeSecretsService; + @InjectMocks + private GramineSessionMakerService gramineSessionService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + when(teeServicesConfig.getPreComputeProperties()).thenReturn(preComputeProperties); + when(teeServicesConfig.getPostComputeProperties()).thenReturn(postComputeProperties); + } + + // region getSessionYml + @Test + void shouldGetSessionJson() throws Exception { + TeeEnclaveConfiguration enclaveConfig = TeeEnclaveConfiguration.builder() + .fingerprint(APP_FINGERPRINT) + .build(); + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + when(postComputeProperties.getFingerprint()).thenReturn(POST_COMPUTE_FINGERPRINT); + when(postComputeProperties.getEntrypoint()).thenReturn(POST_COMPUTE_ENTRYPOINT); + + SecretEnclaveBase appCompute = SecretEnclaveBase.builder() + .name("app") + .mrenclave(APP_FINGERPRINT) + .environment(Map.ofEntries( + Map.entry("IEXEC_TASK_ID", "taskId"), + Map.entry("IEXEC_IN", "/iexec_in"), + Map.entry("IEXEC_OUT", "/iexec_out"), + Map.entry("IEXEC_DATASET_ADDRESS", "0xDatasetAddress"), + Map.entry("IEXEC_DATASET_FILENAME", "datasetName"), + Map.entry("IEXEC_BOT_SIZE", "1"), + Map.entry("IEXEC_BOT_FIRST_INDEX", "0"), + Map.entry("IEXEC_BOT_TASK_INDEX", "0"), + Map.entry("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"), + Map.entry("IEXEC_INPUT_FILES_NUMBER", "2"), + Map.entry("IEXEC_INPUT_FILE_NAME_1", "file1"), + Map.entry("IEXEC_INPUT_FILE_NAME_2", "file2"))) + .build(); + SecretEnclaveBase postCompute = SecretEnclaveBase.builder() + .name("post-compute") + .mrenclave("mrEnclave3") + .environment(Map.ofEntries( + Map.entry("RESULT_TASK_ID", "taskId"), + Map.entry("RESULT_ENCRYPTION", "yes"), + Map.entry("RESULT_ENCRYPTION_PUBLIC_KEY", "encryptionPublicKey"), + Map.entry("RESULT_STORAGE_PROVIDER", "ipfs"), + Map.entry("RESULT_STORAGE_PROXY", "storageProxy"), + Map.entry("RESULT_STORAGE_TOKEN", "storageToken"), + Map.entry("RESULT_STORAGE_CALLBACK", "no"), + Map.entry("RESULT_SIGN_WORKER_ADDRESS", "workerAddress"), + Map.entry("RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY", "teeChallengePrivateKey"))) + .build(); + + when(teeSecretsService.getSecretsTokens(request)) + .thenReturn(SecretSessionBase.builder() + .appCompute(appCompute) + .postCompute(postCompute) + .build()); + + GramineSession actualSpsSession = gramineSessionService.generateSession(request); + log.info(actualSpsSession.toString()); + Map actualJsonMap = new Yaml().load(actualSpsSession.toString()); + String expectedJsonString = FileHelper.readFile("src/test/resources/gramine-tee-session.json"); + Map expectedYmlMap = new Yaml().load(expectedJsonString); + assertRecursively(expectedYmlMap, actualJsonMap); + } + // endregion +} \ No newline at end of file diff --git a/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java b/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java deleted file mode 100644 index fd8b8d82..00000000 --- a/src/test/java/com/iexec/sms/tee/session/palaemon/PalaemonSessionServiceTests.java +++ /dev/null @@ -1,859 +0,0 @@ -/* - * Copyright 2020 IEXEC BLOCKCHAIN TECH - * - * 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. - */ - -package com.iexec.sms.tee.session.palaemon; - -import com.iexec.common.precompute.PreComputeUtils; -import com.iexec.common.task.TaskDescription; -import com.iexec.common.tee.TeeEnclaveConfiguration; -import com.iexec.common.tee.TeeEnclaveConfigurationValidator; -import com.iexec.common.utils.FileHelper; -import com.iexec.common.utils.IexecEnvUtils; -import com.iexec.common.worker.result.ResultUtils; -import com.iexec.sms.api.TeeSessionGenerationError; -import com.iexec.sms.secret.ReservedSecretKeyName; -import com.iexec.sms.secret.Secret; -import com.iexec.sms.secret.compute.OnChainObjectType; -import com.iexec.sms.secret.compute.SecretOwnerRole; -import com.iexec.sms.secret.compute.TeeTaskComputeSecret; -import com.iexec.sms.secret.compute.TeeTaskComputeSecretService; -import com.iexec.sms.secret.web2.Web2SecretsService; -import com.iexec.sms.secret.web3.Web3Secret; -import com.iexec.sms.secret.web3.Web3SecretService; -import com.iexec.sms.tee.challenge.TeeChallenge; -import com.iexec.sms.tee.challenge.TeeChallengeService; -import com.iexec.sms.tee.session.TeeSessionGenerationException; -import com.iexec.sms.tee.session.attestation.AttestationSecurityConfig; -import com.iexec.sms.tee.workflow.TeeWorkflowConfiguration; -import com.iexec.sms.utils.EthereumCredentials; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.ClassUtils; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.test.util.ReflectionTestUtils; -import org.yaml.snakeyaml.Yaml; - -import java.security.GeneralSecurityException; -import java.util.*; - -import static com.iexec.common.chain.DealParams.DROPBOX_RESULT_STORAGE_PROVIDER; -import static com.iexec.common.worker.result.ResultUtils.*; -import static com.iexec.sms.Web3jUtils.createEthereumAddress; -import static com.iexec.sms.api.TeeSessionGenerationError.*; -import static com.iexec.sms.tee.session.palaemon.PalaemonSessionService.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@Slf4j -class PalaemonSessionServiceTests { - - private static final String TEMPLATE_SESSION_FILE = "src/main/resources/palaemonTemplate.vm"; - private static final String EXPECTED_SESSION_FILE = "src/test/resources/tee-session.yml"; - - private static final String TASK_ID = "taskId"; - private static final String SESSION_ID = "sessionId"; - private static final String WORKER_ADDRESS = "workerAddress"; - private static final String ENCLAVE_CHALLENGE = "enclaveChallenge"; - // pre-compute - private static final String PRE_COMPUTE_FINGERPRINT = "mrEnclave1"; - private static final String PRE_COMPUTE_ENTRYPOINT = "entrypoint1"; - private static final String DATASET_ADDRESS = "0xDatasetAddress"; - private static final String DATASET_NAME = "datasetName"; - private static final String DATASET_CHECKSUM = "datasetChecksum"; - private static final String DATASET_URL = "http://datasetUrl"; // 0x687474703a2f2f646174617365742d75726c in hex - // keys with leading/trailing \n should not break the workflow - private static final String DATASET_KEY = "\ndatasetKey\n"; - // app - private static final String APP_DEVELOPER_SECRET_INDEX = "1"; - private static final String APP_DEVELOPER_SECRET_VALUE = "appDeveloperSecretValue"; - private static final String REQUESTER_SECRET_KEY_1 = "requesterSecretKey1"; - private static final String REQUESTER_SECRET_VALUE_1 = "requesterSecretValue1"; - private static final String REQUESTER_SECRET_KEY_2 = "requesterSecretKey2"; - private static final String REQUESTER_SECRET_VALUE_2 = "requesterSecretValue2"; - private static final String APP_URI = "appUri"; - private static final String APP_FINGERPRINT = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"; - private static final String APP_ENTRYPOINT = "appEntrypoint"; - private static final TeeEnclaveConfiguration enclaveConfig = - mock(TeeEnclaveConfiguration.class); - private static final String ARGS = "args"; - private static final String IEXEC_APP_DEVELOPER_SECRET_1 = "IEXEC_APP_DEVELOPER_SECRET_1"; - // post-compute - private static final String POST_COMPUTE_FINGERPRINT = "mrEnclave3"; - private static final String POST_COMPUTE_ENTRYPOINT = "entrypoint3"; - private static final String STORAGE_PROVIDER = "ipfs"; - private static final String STORAGE_PROXY = "storageProxy"; - private static final String STORAGE_TOKEN = "storageToken"; - private static final String ENCRYPTION_PUBLIC_KEY = "encryptionPublicKey"; - private static final String TEE_CHALLENGE_PRIVATE_KEY = "teeChallengePrivateKey"; - // input files - private static final String INPUT_FILE_URL_1 = "http://host/file1"; - private static final String INPUT_FILE_NAME_1 = "file1"; - private static final String INPUT_FILE_URL_2 = "http://host/file2"; - private static final String INPUT_FILE_NAME_2 = "file2"; - - private String appAddress; - private String requesterAddress; - - @Mock - private Web3SecretService web3SecretService; - @Mock - private Web2SecretsService web2SecretsService; - @Mock - private TeeChallengeService teeChallengeService; - @Mock - private TeeWorkflowConfiguration teeWorkflowConfig; - @Mock - private AttestationSecurityConfig attestationSecurityConfig; - @Mock private TeeTaskComputeSecretService teeTaskComputeSecretService; - - private PalaemonSessionService palaemonSessionService; - - @BeforeEach - void beforeEach() { - MockitoAnnotations.openMocks(this); - // spy is needed to mock some internal calls of the tested - // class when relevant - palaemonSessionService = spy(new PalaemonSessionService( - web3SecretService, - web2SecretsService, - teeChallengeService, - teeWorkflowConfig, - attestationSecurityConfig, - teeTaskComputeSecretService - )); - ReflectionTestUtils.setField(palaemonSessionService, "palaemonTemplateFilePath", TEMPLATE_SESSION_FILE); - when(enclaveConfig.getFingerprint()).thenReturn(APP_FINGERPRINT); - when(enclaveConfig.getEntrypoint()).thenReturn(APP_ENTRYPOINT); - } - - //region getSessionYml - @Test - void shouldGetSessionYml() throws Exception { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - doReturn(getPreComputeTokens()).when(palaemonSessionService) - .getPreComputePalaemonTokens(request); - doReturn(getAppTokens()).when(palaemonSessionService) - .getAppPalaemonTokens(request); - doReturn(getPostComputeTokens()).when(palaemonSessionService) - .getPostComputePalaemonTokens(request); - when(attestationSecurityConfig.getToleratedInsecureOptions()) - .thenReturn(List.of("hyperthreading", "debug-mode")); - when(attestationSecurityConfig.getIgnoredSgxAdvisories()) - .thenReturn(List.of("INTEL-SA-00161", "INTEL-SA-00289")); - - String actualYmlString = palaemonSessionService.getSessionYml(request); - Map actualYmlMap = new Yaml().load(actualYmlString); - String expectedYamlString = FileHelper.readFile(EXPECTED_SESSION_FILE); - Map expectedYmlMap = new Yaml().load(expectedYamlString); - assertRecursively(expectedYmlMap, actualYmlMap); - } - - @Test - void shouldNotGetSessionYmlSinceRequestIsNull() { - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getSessionYml(null) - ); - assertEquals(NO_SESSION_REQUEST, exception.getError()); - assertEquals("Session request must not be null", exception.getMessage()); - } - - @Test - void shouldNotGetSessionYmlSinceTaskDescriptionIsMissing() { - PalaemonSessionRequest request = PalaemonSessionRequest.builder().build(); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getSessionYml(request) - ); - assertEquals(NO_TASK_DESCRIPTION, exception.getError()); - assertEquals("Task description must not be null", exception.getMessage()); - } - //endregion - - //region getPreComputePalaemonTokens - @Test - void shouldGetPreComputePalaemonTokens() throws Exception { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - when(teeWorkflowConfig.getPreComputeFingerprint()) - .thenReturn(PRE_COMPUTE_FINGERPRINT); - when(teeWorkflowConfig.getPreComputeEntrypoint()) - .thenReturn(PRE_COMPUTE_ENTRYPOINT); - Web3Secret secret = new Web3Secret(DATASET_ADDRESS, DATASET_KEY); - when(web3SecretService.getSecret(DATASET_ADDRESS, true)) - .thenReturn(Optional.of(secret)); - - Map tokens = - palaemonSessionService.getPreComputePalaemonTokens(request); - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - PalaemonSessionService.PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, - PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, - PreComputeUtils.IEXEC_DATASET_KEY, secret.getTrimmedValue(), - PreComputeUtils.IS_DATASET_REQUIRED, true, - PalaemonSessionService.INPUT_FILE_URLS, - Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2) - - ) - ); - } - - @Test - void shouldGetPreComputePalaemonTokensWithoutDataset() throws Exception { - PalaemonSessionRequest request = PalaemonSessionRequest.builder() - .sessionId(SESSION_ID) - .workerAddress(WORKER_ADDRESS) - .enclaveChallenge(ENCLAVE_CHALLENGE) - .taskDescription(TaskDescription.builder() - .chainTaskId(TASK_ID) - .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) - .build()) - .build(); - when(teeWorkflowConfig.getPreComputeFingerprint()) - .thenReturn(PRE_COMPUTE_FINGERPRINT); - when(teeWorkflowConfig.getPreComputeEntrypoint()) - .thenReturn(PRE_COMPUTE_ENTRYPOINT); - - Map tokens = - palaemonSessionService.getPreComputePalaemonTokens(request); - assertThat(tokens).isNotEmpty() - .containsExactlyInAnyOrderEntriesOf( - Map.of( - PalaemonSessionService.PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, - PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, - PreComputeUtils.IEXEC_DATASET_KEY, "", - PreComputeUtils.IS_DATASET_REQUIRED, false, - PalaemonSessionService.INPUT_FILE_URLS, - Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2) - - ) - ); - } - //endregion - - //region getAppPalaemonTokens - @Test - void shouldGetAppPalaemonTokensForAdvancedTaskDescription() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); - when(enclaveConfig.getValidator()).thenReturn(validator); - when(validator.isValid()).thenReturn(true); - addApplicationDeveloperSecret(); - addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); - addRequesterSecret(REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); - - Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); - - verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX); - verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_1); - verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_2); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - PalaemonSessionService.APP_MRENCLAVE, APP_FINGERPRINT, - PalaemonSessionService.APP_ARGS, APP_ENTRYPOINT + " " + ARGS, - PalaemonSessionService.INPUT_FILE_NAMES, - Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", "file1", - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", "file2" - ), - IEXEC_APP_DEVELOPER_SECRET_1, APP_DEVELOPER_SECRET_VALUE, - REQUESTER_SECRETS, - Map.of( - IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1, - IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "2", REQUESTER_SECRET_VALUE_2 - ) - - ) - ); - } - - @Test - void shouldGetPalaemonTokensWithEmptyAppComputeSecretWhenSecretsDoNotExist() { - final String appAddress = createEthereumAddress(); - final String requesterAddress = createEthereumAddress(); - final TaskDescription taskDescription = TaskDescription.builder() - .chainTaskId(TASK_ID) - .appUri(APP_URI) - .appAddress(appAddress) - .appEnclaveConfiguration(enclaveConfig) - .datasetAddress(DATASET_ADDRESS) - .datasetUri(DATASET_URL) - .datasetName(DATASET_NAME) - .datasetChecksum(DATASET_CHECKSUM) - .requester(requesterAddress) - .cmd(ARGS) - .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) - .isResultEncryption(true) - .resultStorageProvider(STORAGE_PROVIDER) - .resultStorageProxy(STORAGE_PROXY) - .botSize(1) - .botFirstIndex(0) - .botIndex(0) - .build(); - PalaemonSessionRequest request = createSessionRequest(taskDescription); - TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); - when(enclaveConfig.getValidator()).thenReturn(validator); - when(validator.isValid()).thenReturn(true); - when(teeTaskComputeSecretService.getSecret( - OnChainObjectType.APPLICATION, - appAddress, - SecretOwnerRole.APPLICATION_DEVELOPER, - "", - APP_DEVELOPER_SECRET_INDEX)) - .thenReturn(Optional.empty()); - - Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); - verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(appAddress), eq(SecretOwnerRole.APPLICATION_DEVELOPER), eq(""), any()); - verify(teeTaskComputeSecretService, never()).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - PalaemonSessionService.APP_MRENCLAVE, APP_FINGERPRINT, - PalaemonSessionService.APP_ARGS, APP_ENTRYPOINT + " " + ARGS, - PalaemonSessionService.INPUT_FILE_NAMES, - Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", "file1", - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", "file2" - ), - IEXEC_APP_DEVELOPER_SECRET_1, "", - REQUESTER_SECRETS, Collections.emptyMap() - ) - ); - } - - @Test - void shouldFailToGetAppPalaemonTokensSinceNoTaskDescription() { - PalaemonSessionRequest request = PalaemonSessionRequest.builder() - .build(); - TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, - () -> palaemonSessionService.getAppPalaemonTokens(request)); - Assertions.assertEquals(TeeSessionGenerationError.NO_TASK_DESCRIPTION, exception.getError()); - Assertions.assertEquals("Task description must no be null", exception.getMessage()); - } - - @Test - void shouldFailToGetAppPalaemonTokensSinceNoEnclaveConfig() { - PalaemonSessionRequest request = PalaemonSessionRequest.builder() - .sessionId(SESSION_ID) - .workerAddress(WORKER_ADDRESS) - .enclaveChallenge(ENCLAVE_CHALLENGE) - .taskDescription(TaskDescription.builder().build()) - .build(); - TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, - () -> palaemonSessionService.getAppPalaemonTokens(request)); - Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_NO_ENCLAVE_CONFIG, exception.getError()); - Assertions.assertEquals("Enclave configuration must no be null", exception.getMessage()); - } - - @Test - void shouldFailToGetAppPalaemonTokensInvalidEnclaveConfig() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); - when(enclaveConfig.getValidator()).thenReturn(validator); - String validationError = "validation error"; - when(validator.validate()).thenReturn(Collections.singletonList(validationError)); - TeeSessionGenerationException exception = assertThrows(TeeSessionGenerationException.class, - () -> palaemonSessionService.getAppPalaemonTokens(request)); - Assertions.assertEquals(TeeSessionGenerationError.APP_COMPUTE_INVALID_ENCLAVE_CONFIG, exception.getError()); - } - - @Test - void shouldAddMultipleRequesterSecrets() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); - when(enclaveConfig.getValidator()).thenReturn(validator); - when(validator.isValid()).thenReturn(true); - addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); - addRequesterSecret(REQUESTER_SECRET_KEY_2, REQUESTER_SECRET_VALUE_2); - Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); - verify(teeTaskComputeSecretService, times(2)) - .getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); - verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_1); - verify(teeTaskComputeSecretService).getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, REQUESTER_SECRET_KEY_2); - assertThat(tokens).containsEntry(REQUESTER_SECRETS, - Map.of( - IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1, - IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "2", REQUESTER_SECRET_VALUE_2 - )); - } - - @Test - void shouldFilterRequesterSecretIndexLowerThanZero() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - request.getTaskDescription().setSecrets(Map.of("1", REQUESTER_SECRET_KEY_1, "-1", "out-of-bound-requester-secret")); - TeeEnclaveConfigurationValidator validator = mock(TeeEnclaveConfigurationValidator.class); - when(enclaveConfig.getValidator()).thenReturn(validator); - when(validator.isValid()).thenReturn(true); - addRequesterSecret(REQUESTER_SECRET_KEY_1, REQUESTER_SECRET_VALUE_1); - Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getAppPalaemonTokens(request)); - verify(teeTaskComputeSecretService).getSecret(eq(OnChainObjectType.APPLICATION), eq(""), eq(SecretOwnerRole.REQUESTER), any(), any()); - assertThat(tokens).containsEntry(REQUESTER_SECRETS, - Map.of(IexecEnvUtils.IEXEC_REQUESTER_SECRET_PREFIX + "1", REQUESTER_SECRET_VALUE_1)); - } - //endregion - - //region getPostComputePalaemonTokens - @Test - void shouldGetPostComputePalaemonTokens() throws Exception { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - Secret publicKeySecret = new Secret("address", ENCRYPTION_PUBLIC_KEY); - when(teeWorkflowConfig.getPostComputeFingerprint()) - .thenReturn(POST_COMPUTE_FINGERPRINT); - when(teeWorkflowConfig.getPostComputeEntrypoint()) - .thenReturn(POST_COMPUTE_ENTRYPOINT); - when(web2SecretsService.getSecret( - request.getTaskDescription().getBeneficiary(), - ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, - true)) - .thenReturn(Optional.of(publicKeySecret)); - Secret storageSecret = new Secret("address", STORAGE_TOKEN); - when(web2SecretsService.getSecret( - requesterAddress, - ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, - true)) - .thenReturn(Optional.of(storageSecret)); - - TeeChallenge challenge = TeeChallenge.builder() - .credentials(EthereumCredentials.generate()) - .build(); - when(teeChallengeService.getOrCreate(TASK_ID, true)) - .thenReturn(Optional.of(challenge)); - - - Map tokens = - palaemonSessionService.getPostComputePalaemonTokens(request); - - final Map expectedTokens = new HashMap<>(); - expectedTokens.put(PalaemonSessionService.POST_COMPUTE_MRENCLAVE, POST_COMPUTE_FINGERPRINT); - expectedTokens.put(PalaemonSessionService.POST_COMPUTE_ENTRYPOINT, POST_COMPUTE_ENTRYPOINT); - // encryption tokens - expectedTokens.put(ResultUtils.RESULT_ENCRYPTION, "yes"); - expectedTokens.put(ResultUtils.RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY); - // storage tokens - expectedTokens.put(ResultUtils.RESULT_STORAGE_CALLBACK, "no"); - expectedTokens.put(ResultUtils.RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER); - expectedTokens.put(ResultUtils.RESULT_STORAGE_PROXY, STORAGE_PROXY); - expectedTokens.put(ResultUtils.RESULT_STORAGE_TOKEN, STORAGE_TOKEN); - // sign tokens - expectedTokens.put(ResultUtils.RESULT_TASK_ID, TASK_ID); - expectedTokens.put(ResultUtils.RESULT_SIGN_WORKER_ADDRESS, WORKER_ADDRESS); - expectedTokens.put(ResultUtils.RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, challenge.getCredentials().getPrivateKey()); - - assertThat(tokens).containsExactlyEntriesOf(expectedTokens); - } - - @Test - void shouldNotGetPostComputePalaemonTokensSinceTaskDescriptionMissing() { - PalaemonSessionRequest request = PalaemonSessionRequest.builder().build(); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputePalaemonTokens(request) - ); - assertEquals(NO_TASK_DESCRIPTION, exception.getError()); - assertEquals("Task description must not be null", exception.getMessage()); - } - //endregion - - //region getPostComputeEncryptionTokens - @Test - void shouldGetPostComputeStorageTokensWithCallback() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - sessionRequest.getTaskDescription().setCallback("callback"); - - final Map tokens = assertDoesNotThrow( - () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_STORAGE_CALLBACK, "yes", - RESULT_STORAGE_PROVIDER, EMPTY_YML_VALUE, - RESULT_STORAGE_PROXY, EMPTY_YML_VALUE, - RESULT_STORAGE_TOKEN, EMPTY_YML_VALUE - ) - ); - } - - @Test - void shouldGetPostComputeStorageTokensOnIpfs() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final TaskDescription taskDescription = sessionRequest.getTaskDescription(); - - final String secretValue = "Secret value"; - when(web2SecretsService - .getSecret(taskDescription.getRequester(), ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, true)) - .thenReturn(Optional.of(new Secret(null, secretValue))); - - final Map tokens = assertDoesNotThrow( - () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_STORAGE_CALLBACK, "no", - RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER, - RESULT_STORAGE_PROXY, STORAGE_PROXY, - RESULT_STORAGE_TOKEN, secretValue - ) - ); - } - - @Test - void shouldGetPostComputeStorageTokensOnDropbox() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final TaskDescription taskDescription = sessionRequest.getTaskDescription(); - taskDescription.setResultStorageProvider(DROPBOX_RESULT_STORAGE_PROVIDER); - - final String secretValue = "Secret value"; - when(web2SecretsService - .getSecret(taskDescription.getRequester(), ReservedSecretKeyName.IEXEC_RESULT_DROPBOX_TOKEN, true)) - .thenReturn(Optional.of(new Secret(null, secretValue))); - - final Map tokens = assertDoesNotThrow( - () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_STORAGE_CALLBACK, "no", - RESULT_STORAGE_PROVIDER, DROPBOX_RESULT_STORAGE_PROVIDER, - RESULT_STORAGE_PROXY, STORAGE_PROXY, - RESULT_STORAGE_TOKEN, secretValue - ) - ); - } - - @Test - void shouldNotGetPostComputeStorageTokensSinceNoSecret() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final TaskDescription taskDescription = sessionRequest.getTaskDescription(); - - when(web2SecretsService - .getSecret(taskDescription.getRequester(), ReservedSecretKeyName.IEXEC_RESULT_IEXEC_IPFS_TOKEN, true)) - .thenReturn(Optional.empty()); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeStorageTokens(sessionRequest)); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_STORAGE_TOKENS_FAILED); - assertThat(exception.getMessage()).isEqualTo("Empty requester storage token - taskId: " + taskDescription.getChainTaskId()); - } - - @Test - void shouldGetPostComputeSignTokens() throws GeneralSecurityException { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final TaskDescription taskDescription = sessionRequest.getTaskDescription(); - final String taskId = taskDescription.getChainTaskId(); - final EthereumCredentials credentials = EthereumCredentials.generate(); - - when(teeChallengeService.getOrCreate(taskId, true)) - .thenReturn(Optional.of(TeeChallenge.builder().credentials(credentials).build())); - - final Map tokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeSignTokens(sessionRequest)); - - assertThat(tokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_TASK_ID, taskId, - RESULT_SIGN_WORKER_ADDRESS, sessionRequest.getWorkerAddress(), - RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, credentials.getPrivateKey() - ) - ); - } - - @ParameterizedTest - @NullSource - @ValueSource(strings = {""}) - void shouldNotGetPostComputeSignTokensSinceNoWorkerAddress(String emptyWorkerAddress) { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); - sessionRequest.setWorkerAddress(emptyWorkerAddress); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) - ); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_WORKER_ADDRESS); - assertThat(exception.getMessage()).isEqualTo("Empty worker address - taskId: " + taskId); - } - - @ParameterizedTest - @NullSource - @ValueSource(strings = {""}) - void shouldNotGetPostComputeSignTokensSinceNoEnclaveChallenge(String emptyEnclaveChallenge) { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); - sessionRequest.setEnclaveChallenge(emptyEnclaveChallenge); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) - ); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_PUBLIC_ENCLAVE_CHALLENGE); - assertThat(exception.getMessage()).isEqualTo("Empty public enclave challenge - taskId: " + taskId); - } - - @Test - void shouldNotGetPostComputeSignTokensSinceNoTeeChallenge() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); - - when(teeChallengeService.getOrCreate(taskId, true)) - .thenReturn(Optional.empty()); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) - ); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CHALLENGE); - assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge - taskId: " + taskId); - } - - @Test - void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentials() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); - - when(teeChallengeService.getOrCreate(taskId, true)) - .thenReturn(Optional.of(TeeChallenge.builder().credentials(null).build())); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) - ); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); - assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); - } - - @Test - void shouldNotGetPostComputeSignTokensSinceNoEnclaveCredentialsPrivateKey() { - final PalaemonSessionRequest sessionRequest = createSessionRequest(createTaskDescription()); - final String taskId = sessionRequest.getTaskDescription().getChainTaskId(); - - when(teeChallengeService.getOrCreate(taskId, true)) - .thenReturn(Optional.of(TeeChallenge.builder().credentials(new EthereumCredentials("", "", false, "")).build())); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeSignTokens(sessionRequest) - ); - - assertThat(exception.getError()).isEqualTo(POST_COMPUTE_GET_SIGNATURE_TOKENS_FAILED_EMPTY_TEE_CREDENTIALS); - assertThat(exception.getMessage()).isEqualTo("Empty TEE challenge credentials - taskId: " + taskId); - } - - // endregion - - @Test - void shouldGetPostComputeEncryptionTokensWithEncryption() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - - Secret publicKeySecret = new Secret("address", ENCRYPTION_PUBLIC_KEY); - when(web2SecretsService.getSecret( - request.getTaskDescription().getBeneficiary(), - ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, - true)) - .thenReturn(Optional.of(publicKeySecret)); - - final Map encryptionTokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeEncryptionTokens(request)); - assertThat(encryptionTokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_ENCRYPTION, "yes", - RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY - ) - ); - } - - @Test - void shouldGetPostComputeEncryptionTokensWithoutEncryption() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - request.getTaskDescription().setResultEncryption(false); - - final Map encryptionTokens = assertDoesNotThrow(() -> palaemonSessionService.getPostComputeEncryptionTokens(request)); - assertThat(encryptionTokens) - .containsExactlyInAnyOrderEntriesOf( - Map.of( - RESULT_ENCRYPTION, "no", - RESULT_ENCRYPTION_PUBLIC_KEY, "" - ) - ); - } - - @Test - void shouldNotGetPostComputeEncryptionTokensSinceEmptyBeneficiaryKey() { - PalaemonSessionRequest request = createSessionRequest(createTaskDescription()); - - when(web2SecretsService.getSecret( - request.getTaskDescription().getBeneficiary(), - ReservedSecretKeyName.IEXEC_RESULT_ENCRYPTION_PUBLIC_KEY, - true)) - .thenReturn(Optional.empty()); - - final TeeSessionGenerationException exception = assertThrows( - TeeSessionGenerationException.class, - () -> palaemonSessionService.getPostComputeEncryptionTokens(request) - ); - assertEquals(POST_COMPUTE_GET_ENCRYPTION_TOKENS_FAILED_EMPTY_BENEFICIARY_KEY, exception.getError()); - assertEquals("Empty beneficiary encryption key - taskId: taskId", exception.getMessage()); - } - - //endregion - - //region utils - private void addApplicationDeveloperSecret() { - TeeTaskComputeSecret applicationDeveloperSecret = TeeTaskComputeSecret.builder() - .onChainObjectType(OnChainObjectType.APPLICATION) - .onChainObjectAddress(appAddress) - .secretOwnerRole(SecretOwnerRole.APPLICATION_DEVELOPER) - .key(APP_DEVELOPER_SECRET_INDEX) - .value(APP_DEVELOPER_SECRET_VALUE) - .build(); - when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, appAddress, SecretOwnerRole.APPLICATION_DEVELOPER, "", APP_DEVELOPER_SECRET_INDEX)) - .thenReturn(Optional.of(applicationDeveloperSecret)); - } - - private void addRequesterSecret(String secretKey, String secretValue) { - TeeTaskComputeSecret requesterSecret = TeeTaskComputeSecret.builder() - .onChainObjectType(OnChainObjectType.APPLICATION) - .onChainObjectAddress("") - .secretOwnerRole(SecretOwnerRole.REQUESTER) - .fixedSecretOwner(requesterAddress) - .key(secretKey) - .value(secretValue) - .build(); - when(teeTaskComputeSecretService.getSecret(OnChainObjectType.APPLICATION, "", SecretOwnerRole.REQUESTER, requesterAddress, secretKey)) - .thenReturn(Optional.of(requesterSecret)); - } - - private PalaemonSessionRequest createSessionRequest(TaskDescription taskDescription) { - return PalaemonSessionRequest.builder() - .sessionId(SESSION_ID) - .workerAddress(WORKER_ADDRESS) - .enclaveChallenge(ENCLAVE_CHALLENGE) - .taskDescription(taskDescription) - .build(); - } - - private TaskDescription createTaskDescription() { - appAddress = createEthereumAddress(); - requesterAddress = createEthereumAddress(); - return TaskDescription.builder() - .chainTaskId(TASK_ID) - .appUri(APP_URI) - .appAddress(appAddress) - .appEnclaveConfiguration(enclaveConfig) - .datasetAddress(DATASET_ADDRESS) - .datasetUri(DATASET_URL) - .datasetName(DATASET_NAME) - .datasetChecksum(DATASET_CHECKSUM) - .requester(requesterAddress) - .cmd(ARGS) - .inputFiles(List.of(INPUT_FILE_URL_1, INPUT_FILE_URL_2)) - .isResultEncryption(true) - .resultStorageProvider(STORAGE_PROVIDER) - .resultStorageProxy(STORAGE_PROXY) - .secrets(Map.of("1", REQUESTER_SECRET_KEY_1, "2", REQUESTER_SECRET_KEY_2)) - .botSize(1) - .botFirstIndex(0) - .botIndex(0) - .build(); - } - - private Map getPreComputeTokens() { - return Map.of( - PRE_COMPUTE_MRENCLAVE, PRE_COMPUTE_FINGERPRINT, - PalaemonSessionService.PRE_COMPUTE_ENTRYPOINT, PRE_COMPUTE_ENTRYPOINT, - PreComputeUtils.IS_DATASET_REQUIRED, true, - PreComputeUtils.IEXEC_DATASET_KEY, DATASET_KEY.trim(), - INPUT_FILE_URLS, Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "1", INPUT_FILE_URL_1, - IexecEnvUtils.IEXEC_INPUT_FILE_URL_PREFIX + "2", INPUT_FILE_URL_2)); - } - - private Map getAppTokens() { - return Map.of( - APP_MRENCLAVE, APP_FINGERPRINT, - APP_ARGS, APP_ENTRYPOINT + " " + ARGS, - INPUT_FILE_NAMES, Map.of( - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "1", INPUT_FILE_NAME_1, - IexecEnvUtils.IEXEC_INPUT_FILE_NAME_PREFIX + "2", INPUT_FILE_NAME_2)); - } - - private Map getPostComputeTokens() { - Map map = new HashMap<>(); - map.put(POST_COMPUTE_MRENCLAVE, POST_COMPUTE_FINGERPRINT); - map.put(PalaemonSessionService.POST_COMPUTE_ENTRYPOINT, POST_COMPUTE_ENTRYPOINT); - map.put(RESULT_TASK_ID, TASK_ID); - map.put(RESULT_ENCRYPTION, "yes"); - map.put(RESULT_ENCRYPTION_PUBLIC_KEY, ENCRYPTION_PUBLIC_KEY); - map.put(RESULT_STORAGE_PROVIDER, STORAGE_PROVIDER); - map.put(RESULT_STORAGE_PROXY, STORAGE_PROXY); - map.put(RESULT_STORAGE_TOKEN, STORAGE_TOKEN); - map.put(RESULT_STORAGE_CALLBACK, "no"); - map.put(RESULT_SIGN_WORKER_ADDRESS, WORKER_ADDRESS); - map.put(RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY, TEE_CHALLENGE_PRIVATE_KEY); - return map; - } - - private void assertRecursively(Object expected, Object actual) { - if (expected == null || - expected instanceof String || - ClassUtils.isPrimitiveOrWrapper(expected.getClass())) { - log.info("Comparing [actual:{}, expected:{}]", expected, actual); - assertThat(expected).isEqualTo(actual); - return; - } - if (expected instanceof List) { - List actualList = (List) expected; - List expectedList = (List) actual; - for (int i = 0; i < actualList.size(); i++) { - assertRecursively(actualList.get(i), expectedList.get(i)); - } - return; - } - if (expected instanceof Map) { - Map actualMap = (Map) expected; - Map expectedMap = (Map) actual; - actualMap.keySet().forEach((key) -> { - log.info("Checking '{}'", key); - assertRecursively(actualMap.get(key), expectedMap.get(key)); - }); - } - } - //endregion -} diff --git a/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerServiceTests.java b/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerServiceTests.java new file mode 100644 index 00000000..a726ec46 --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionHandlerServiceTests.java @@ -0,0 +1,140 @@ +package com.iexec.sms.tee.session.scone; + +import com.iexec.common.task.TaskDescription; +import com.iexec.sms.api.TeeSessionGenerationError; +import com.iexec.sms.tee.session.TeeSessionLogConfiguration; +import com.iexec.sms.tee.session.generic.TeeSessionGenerationException; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.scone.cas.CasClient; +import com.iexec.sms.tee.session.scone.cas.CasConfiguration; +import com.iexec.sms.tee.session.scone.cas.SconeSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.http.ResponseEntity; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(OutputCaptureExtension.class) +class SconeSessionHandlerServiceTests { + + private static final String CAS_URL = "casUrl"; + @Mock + private SconeSessionMakerService sessionService; + @Mock + private CasClient apiClient; + @Mock + private TeeSessionLogConfiguration teeSessionLogConfiguration; + @Mock + private CasConfiguration casConfiguration; + @InjectMocks + private SconeSessionHandlerService sessionHandlerService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + when(casConfiguration.getEnclaveHost()).thenReturn(CAS_URL); + } + + @Test + void shouldBuildAndPostSessionWithLogs(CapturedOutput output) + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + SconeSession casSession = mock(SconeSession.class); + when(casSession.toString()).thenReturn("sessionContent"); + when(sessionService.generateSession(request)).thenReturn(casSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) + .thenReturn(true); + when(apiClient.postSession(casSession.toString())) + .thenReturn(ResponseEntity.created(null).body("sessionId")); + + assertEquals(CAS_URL, + sessionHandlerService.buildAndPostSession(request)); + // Testing output here since it reflects a business feature (ability to + // catch a + // session in debug mode) + assertTrue(output.getOut() + .contains("Session content [taskId:null]\nsessionContent\n")); + } + + @Test + void shouldBuildAndPostSessionWithoutLogs(CapturedOutput output) + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + SconeSession casSession = mock(SconeSession.class); + when(casSession.toString()).thenReturn("sessionContent"); + when(sessionService.generateSession(request)).thenReturn(casSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) + .thenReturn(false); + when(apiClient.postSession(casSession.toString())) + .thenReturn(ResponseEntity.created(null).body("sessionId")); + + assertEquals(CAS_URL, + sessionHandlerService.buildAndPostSession(request)); + // Testing output here since it reflects a business feature (ability to + // catch a + // session in debug mode) + assertTrue(output.getOut().isEmpty()); + } + + @Test + void shouldNotBuildAndPostSessionSinceBuildSessionFailed() + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TeeSessionGenerationException teeSessionGenerationException = new TeeSessionGenerationException( + TeeSessionGenerationError.SECURE_SESSION_GENERATION_FAILED, + "some error"); + when(sessionService.generateSession(request)) + .thenThrow(teeSessionGenerationException); + + assertThrows(teeSessionGenerationException.getClass(), + () -> sessionHandlerService.buildAndPostSession(request)); + } + + @Test + void shouldNotBuildAndPostSessionSincePostSessionFailed() + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + SconeSession casSession = mock(SconeSession.class); + when(sessionService.generateSession(request)).thenReturn(casSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) + .thenReturn(true); + when(apiClient.postSession(casSession.toString())) + .thenReturn(ResponseEntity.internalServerError().build()); + + assertThrows(TeeSessionGenerationException.class, + () -> sessionHandlerService.buildAndPostSession(request)); + } + + @Test + void shouldNotBuildAndPostSessionSinceNoResponse() + throws TeeSessionGenerationException { + TeeSessionRequest request = mock(TeeSessionRequest.class); + TaskDescription taskDescription = mock(TaskDescription.class); + when(request.getTaskDescription()).thenReturn(taskDescription); + SconeSession casSession = mock(SconeSession.class); + when(sessionService.generateSession(request)).thenReturn(casSession); + when(teeSessionLogConfiguration.isDisplayDebugSessionEnabled()) + .thenReturn(true); + when(apiClient.postSession(casSession.toString())) + .thenReturn(null); + + assertThrows(TeeSessionGenerationException.class, + () -> sessionHandlerService.buildAndPostSession(request)); + } +} diff --git a/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionMakerServiceTests.java b/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionMakerServiceTests.java new file mode 100644 index 00000000..1f7e77df --- /dev/null +++ b/src/test/java/com/iexec/sms/tee/session/scone/SconeSessionMakerServiceTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2020 IEXEC BLOCKCHAIN TECH + * + * 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. + */ + +package com.iexec.sms.tee.session.scone; + +import com.iexec.common.tee.TeeEnclaveConfiguration; +import com.iexec.common.utils.FileHelper; +import com.iexec.sms.api.config.SconeServicesProperties; +import com.iexec.sms.api.config.TeeAppProperties; +import com.iexec.sms.tee.session.base.SecretEnclaveBase; +import com.iexec.sms.tee.session.base.SecretSessionBase; +import com.iexec.sms.tee.session.base.SecretSessionBaseService; +import com.iexec.sms.tee.session.generic.TeeSessionRequest; +import com.iexec.sms.tee.session.scone.cas.SconeSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.yaml.snakeyaml.Yaml; + +import java.util.List; +import java.util.Map; + +import static com.iexec.sms.tee.session.TeeSessionTestUtils.*; +import static org.mockito.Mockito.when; + +class SconeSessionMakerServiceTests { + + private static final String PRE_COMPUTE_ENTRYPOINT = "entrypoint1"; + private static final String APP_FINGERPRINT = "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b"; + private static final String APP_ENTRYPOINT = "appEntrypoint"; + private static final String POST_COMPUTE_ENTRYPOINT = "entrypoint3"; + + @Mock + private TeeAppProperties preComputeProperties; + @Mock + private TeeAppProperties postComputeProperties; + @Mock + private SconeServicesProperties teeServicesConfig; + @Mock + private SecretSessionBaseService teeSecretsService; + @Mock + private SconeSessionSecurityConfig attestationSecurityConfig; + + @InjectMocks + private SconeSessionMakerService palaemonSessionService; + + @BeforeEach + void beforeEach() { + MockitoAnnotations.openMocks(this); + when(teeServicesConfig.getPreComputeProperties()).thenReturn(preComputeProperties); + when(teeServicesConfig.getPostComputeProperties()).thenReturn(postComputeProperties); + } + + // region getSessionYml + @Test + void shouldGetSessionYml() throws Exception { + TeeEnclaveConfiguration enclaveConfig = TeeEnclaveConfiguration.builder() + .fingerprint(APP_FINGERPRINT) + .entrypoint(APP_ENTRYPOINT) + .build(); + TeeSessionRequest request = createSessionRequest(createTaskDescription(enclaveConfig)); + + when(preComputeProperties.getEntrypoint()).thenReturn(PRE_COMPUTE_ENTRYPOINT); + when(postComputeProperties.getEntrypoint()).thenReturn(POST_COMPUTE_ENTRYPOINT); + + SecretEnclaveBase preCompute = SecretEnclaveBase.builder() + .name("pre-compute") + .mrenclave("mrEnclave1") + .environment(Map.ofEntries( + // Keeping these test env vars for now + // (could be less but keeping same resource file for now) + Map.entry("IEXEC_TASK_ID", "taskId"), + Map.entry("IEXEC_PRE_COMPUTE_OUT", "/iexec_in"), + Map.entry("IS_DATASET_REQUIRED", "true"), + Map.entry("IEXEC_DATASET_KEY", "datasetKey"), + Map.entry("IEXEC_DATASET_URL", "http://datasetUrl"), + Map.entry("IEXEC_DATASET_FILENAME", "datasetName"), + Map.entry("IEXEC_DATASET_CHECKSUM", "datasetChecksum"), + Map.entry("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"), + Map.entry("IEXEC_INPUT_FILES_NUMBER", "2"), + Map.entry("IEXEC_INPUT_FILE_URL_1", "http://host/file1"), + Map.entry("IEXEC_INPUT_FILE_URL_2", "http://host/file2"))) + .build(); + SecretEnclaveBase appCompute = SecretEnclaveBase.builder() + .name("app") + .mrenclave(APP_FINGERPRINT) + .environment(Map.ofEntries( + Map.entry("IEXEC_TASK_ID", "taskId"), + Map.entry("IEXEC_IN", "/iexec_in"), + Map.entry("IEXEC_OUT", "/iexec_out"), + Map.entry("IEXEC_DATASET_ADDRESS", "0xDatasetAddress"), + Map.entry("IEXEC_DATASET_FILENAME", "datasetName"), + Map.entry("IEXEC_BOT_SIZE", "1"), + Map.entry("IEXEC_BOT_FIRST_INDEX", "0"), + Map.entry("IEXEC_BOT_TASK_INDEX", "0"), + Map.entry("IEXEC_INPUT_FILES_FOLDER", "/iexec_in"), + Map.entry("IEXEC_INPUT_FILES_NUMBER", "2"), + Map.entry("IEXEC_INPUT_FILE_NAME_1", "file1"), + Map.entry("IEXEC_INPUT_FILE_NAME_2", "file2"))) + .build(); + SecretEnclaveBase postCompute = SecretEnclaveBase.builder() + .name("post-compute") + .mrenclave("mrEnclave3") + .environment(Map.ofEntries( + Map.entry("RESULT_TASK_ID", "taskId"), + Map.entry("RESULT_ENCRYPTION", "yes"), + Map.entry("RESULT_ENCRYPTION_PUBLIC_KEY", "encryptionPublicKey"), + Map.entry("RESULT_STORAGE_PROVIDER", "ipfs"), + Map.entry("RESULT_STORAGE_PROXY", "storageProxy"), + Map.entry("RESULT_STORAGE_TOKEN", "storageToken"), + Map.entry("RESULT_STORAGE_CALLBACK", "no"), + Map.entry("RESULT_SIGN_WORKER_ADDRESS", "workerAddress"), + Map.entry("RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY", "teeChallengePrivateKey"))) + .build(); + + when(teeSecretsService.getSecretsTokens(request)) + .thenReturn(SecretSessionBase.builder() + .preCompute(preCompute) + .appCompute(appCompute) + .postCompute(postCompute) + .build()); + + when(attestationSecurityConfig.getToleratedInsecureOptions()) + .thenReturn(List.of("hyperthreading", "debug-mode")); + when(attestationSecurityConfig.getIgnoredSgxAdvisories()) + .thenReturn(List.of("INTEL-SA-00161", "INTEL-SA-00289")); + + when(teeSecretsService.getSecretsTokens(request)) + .thenReturn(SecretSessionBase.builder() + .preCompute(preCompute) + .appCompute(appCompute) + .postCompute(postCompute) + .build()); + + SconeSession actualCasSession = palaemonSessionService.generateSession(request); + System.out.println(actualCasSession.toString()); + Map actualYmlMap = new Yaml().load(actualCasSession.toString()); + String expectedYamlString = FileHelper.readFile("src/test/resources/palaemon-tee-session.yml"); + Map expectedYmlMap = new Yaml().load(expectedYamlString); + assertRecursively(expectedYmlMap, actualYmlMap); + } + // endregion +} diff --git a/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java b/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java deleted file mode 100644 index fd930878..00000000 --- a/src/test/java/com/iexec/sms/tee/workflow/TeeWorkflowConfigurationTests.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.iexec.sms.tee.workflow; - -import com.iexec.common.tee.TeeWorkflowSharedConfiguration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.util.unit.DataSize; - -import static org.assertj.core.api.Assertions.assertThat; - -class TeeWorkflowConfigurationTests { - - private static final String LAS_IMAGE = "lasImage"; - private static final String PRE_COMPUTE_IMAGE = "preComputeImage"; - private static final String PRE_COMPUTE_FINGERPRINT = "preComputeFingerprint"; - private static final String PRE_COMPUTE_ENTRYPOINT = "preComputeEntrypoint"; - private static final int PRE_COMPUTE_HEAP_GB = 1; - private static final String POST_COMPUTE_IMAGE = "postComputeImage"; - private static final String POST_COMPUTE_FINGERPRINT = "postComputeFingerprint"; - private static final String POST_COMPUTE_ENTRYPOINT = "postComputeEntrypoint"; - private static final int POST_COMPUTE_HEAP_GB = 2; - - TeeWorkflowConfiguration teeWorkflowConfiguration = new TeeWorkflowConfiguration(null); - - @BeforeEach - void beforeEach() { - ReflectionTestUtils.setField(teeWorkflowConfiguration, "lasImage", LAS_IMAGE); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "preComputeImage", PRE_COMPUTE_IMAGE); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "preComputeFingerprint", PRE_COMPUTE_FINGERPRINT); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "preComputeEntrypoint", PRE_COMPUTE_ENTRYPOINT); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "preComputeHeapSizeGb", PRE_COMPUTE_HEAP_GB); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "postComputeImage", POST_COMPUTE_IMAGE); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "postComputeFingerprint", POST_COMPUTE_FINGERPRINT); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "postComputeEntrypoint", POST_COMPUTE_ENTRYPOINT); - ReflectionTestUtils.setField(teeWorkflowConfiguration, "postComputeHeapSizeGb", POST_COMPUTE_HEAP_GB); - } - - @Test - void shouldGetPublicConfiguration() { - assertThat(teeWorkflowConfiguration.getSharedConfiguration()) - .isEqualTo(TeeWorkflowSharedConfiguration.builder() - .lasImage(LAS_IMAGE) - .preComputeImage(PRE_COMPUTE_IMAGE) - .preComputeEntrypoint(PRE_COMPUTE_ENTRYPOINT) - .preComputeHeapSize(DataSize.ofGigabytes(PRE_COMPUTE_HEAP_GB).toBytes()) - .postComputeImage(POST_COMPUTE_IMAGE) - .postComputeEntrypoint(POST_COMPUTE_ENTRYPOINT) - .postComputeHeapSize(DataSize.ofGigabytes(POST_COMPUTE_HEAP_GB).toBytes()) - .build()); - } -} diff --git a/src/test/resources/gramine-tee-session.json b/src/test/resources/gramine-tee-session.json new file mode 100644 index 00000000..2e89e314 --- /dev/null +++ b/src/test/resources/gramine-tee-session.json @@ -0,0 +1,45 @@ +{ + "session": "sessionId", + "enclaves": [ + { + "name": "app", + "mrenclave": "01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b", + "command": "", + "environment": { + "IEXEC_TASK_ID": "taskId", + "IEXEC_IN": "/iexec_in", + "IEXEC_OUT": "/iexec_out", + + "IEXEC_DATASET_ADDRESS": "0xDatasetAddress", + "IEXEC_DATASET_FILENAME": "datasetName", + + "IEXEC_BOT_SIZE": "1", + "IEXEC_BOT_FIRST_INDEX": "0", + "IEXEC_BOT_TASK_INDEX": "0", + + "IEXEC_INPUT_FILES_FOLDER": "/iexec_in", + "IEXEC_INPUT_FILES_NUMBER": "2", + "IEXEC_INPUT_FILE_NAME_1": "file1", + "IEXEC_INPUT_FILE_NAME_2": "file2" + }, + "volumes": [] + }, + { + "name": "post-compute", + "mrenclave": "mrEnclave3", + "command": "", + "environment": { + "RESULT_TASK_ID": "taskId", + "RESULT_ENCRYPTION": "yes", + "RESULT_ENCRYPTION_PUBLIC_KEY": "encryptionPublicKey", + "RESULT_STORAGE_PROVIDER": "ipfs", + "RESULT_STORAGE_PROXY": "storageProxy", + "RESULT_STORAGE_TOKEN": "storageToken", + "RESULT_STORAGE_CALLBACK": "no", + "RESULT_SIGN_WORKER_ADDRESS": "workerAddress", + "RESULT_SIGN_TEE_CHALLENGE_PRIVATE_KEY": "teeChallengePrivateKey" + }, + "volumes": [] + } + ] +} \ No newline at end of file diff --git a/src/test/resources/tee-session.yml b/src/test/resources/palaemon-tee-session.yml similarity index 100% rename from src/test/resources/tee-session.yml rename to src/test/resources/palaemon-tee-session.yml