diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b3fc9b7a..ebc18fed 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -33,6 +33,8 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/assets/**' +permissions: read-all + jobs: analyze: name: Analyze @@ -52,7 +54,7 @@ jobs: with: java-version: 11 - name: Initialize CodeQL - uses: github/codeql-action/init@46110c361b7e9ea1b6f9c6ba2cc941fa7a106cca + uses: github/codeql-action/init@474bbf07f9247ffe1856c6a0f94aeeb10e7afee6 #v1.1.0 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -62,6 +64,6 @@ jobs: - name: Build with Gradle run: ./gradlew build -x test -x dockerClean -x dockerPrepare -x dockerRun -x dockerRunStatus -x dockerStop - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@46110c361b7e9ea1b6f9c6ba2cc941fa7a106cca + uses: github/codeql-action/analyze@474bbf07f9247ffe1856c6a0f94aeeb10e7afee6 #v1.1.0 - name: Check dependencies with Gradle run: ./gradlew ossIndexAudit -PossIndexUsername=${{ secrets.OSS_INDEX_USER }} -PossIndexPassword=${{ secrets.OSS_INDEX_PASSWORD }} diff --git a/.github/workflows/gradle-ci.yml b/.github/workflows/gradle-ci.yml index e7a0eefb..14958687 100644 --- a/.github/workflows/gradle-ci.yml +++ b/.github/workflows/gradle-ci.yml @@ -16,6 +16,8 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/assets/**' +permissions: read-all + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/gradle-oss-index-scan.yml b/.github/workflows/gradle-oss-index-scan.yml index a19127b5..0aff7ad0 100644 --- a/.github/workflows/gradle-oss-index-scan.yml +++ b/.github/workflows/gradle-oss-index-scan.yml @@ -6,6 +6,8 @@ on: # * is a special character in YAML, so we have to quote this string - cron: '0 7 * * 2,4,6' +permissions: read-all + jobs: analyze: name: Analyze diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0377afd2..da969978 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -16,6 +16,8 @@ on: - '.github/ISSUE_TEMPLATE/**' - '.github/assets/**' +permissions: read-all + jobs: build: runs-on: ubuntu-latest diff --git a/README.md b/README.md index c7993a0c..f2219c05 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,19 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo - ```AES``` (```128```/```192```/```256```) - ```AES-CBC``` - ```AES-CBC Pad``` +- Sing/Verify digest with keys + - ```RSA``` (```2k```/```3k```/```4k```) + - ```PS256``` + - ```PS384``` + - ```PS512``` + - ```RS256``` + - ```RS384``` + - ```RS512``` + - ```EC``` (```P-256```/```P-256K```/```P-384```/```P-521```) + - ```ES256``` + - ```ES256K``` + - ```ES384``` + - ```ES512``` ### Secrets @@ -147,3 +160,11 @@ when starting the container should do the trick. ##### Using Testcontainers This issue should not happen while using Testcontainers. See examples under [Lowkey Vault Testcontainers](lowkey-vault-testcontainers/README.md). + +# Limitations + +- Some encryption/signature algorithms are not supported. Please refer to the ["Features"](#features) section for the up-to-date list of supported algorithms. +- ```EC``` public keys are not exposed by the server to make Azure Cryptography Client always go to the server during encrypt/verify operations. +- Backup and restore features are not supported at the moment +- Certificate Vault features are not supported at the moment +- Recovery options cannot be set as vault creation is implicit during start-up diff --git a/lowkey-vault-app/build.gradle b/lowkey-vault-app/build.gradle index 34605cf7..32b2a5a2 100644 --- a/lowkey-vault-app/build.gradle +++ b/lowkey-vault-app/build.gradle @@ -35,6 +35,9 @@ test { outputs.file(file("$buildDir/reports/abort-mission/abort-mission-report.json")) useJUnitPlatform() systemProperty("junit.jupiter.extensions.autodetection.enabled", true) + systemProperty("junit.jupiter.execution.parallel.enabled", true) + systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent") } abortMission { toolVersion rootProject.ext.abortMissionVersion diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java index 07db3eb9..13357853 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyController.java @@ -6,9 +6,7 @@ import com.github.nagyesta.lowkeyvault.model.common.ApiConstants; import com.github.nagyesta.lowkeyvault.model.common.KeyVaultItemListModel; import com.github.nagyesta.lowkeyvault.model.v7_2.key.*; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.CreateKeyRequest; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.KeyOperationsParameters; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.UpdateKeyRequest; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.*; import com.github.nagyesta.lowkeyvault.service.key.KeyVaultFake; import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity; import com.github.nagyesta.lowkeyvault.service.key.id.KeyEntityId; @@ -238,6 +236,41 @@ public ResponseEntity decrypt( return ResponseEntity.ok(KeyOperationsResult.forBytes(keyVaultKeyEntity.getId(), decrypted, request)); } + @PostMapping(value = "/keys/{keyName}/{keyVersion}/sign", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity sign( + @PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName, + @PathVariable @Valid @Pattern(regexp = VERSION_NAME_PATTERN) final String keyVersion, + @RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri, + @Valid @RequestBody final KeySignParameters request) { + log.info("Received request to {} sign using key: {} with version: {} using API version: {}", + baseUri.toString(), keyName, keyVersion, V_7_2); + + final ReadOnlyKeyVaultKeyEntity keyVaultKeyEntity = getEntityByNameAndVersion(baseUri, keyName, keyVersion); + final byte[] signature = keyVaultKeyEntity.signBytes(request.getValueAsBase64DecodedBytes(), request.getAlgorithm()); + return ResponseEntity.ok(KeySignResult.forBytes(keyVaultKeyEntity.getId(), signature)); + } + + @PostMapping(value = "/keys/{keyName}/{keyVersion}/verify", + params = API_VERSION_7_2, + consumes = APPLICATION_JSON_VALUE, + produces = APPLICATION_JSON_VALUE) + public ResponseEntity verify( + @PathVariable @Valid @Pattern(regexp = NAME_PATTERN) final String keyName, + @PathVariable @Valid @Pattern(regexp = VERSION_NAME_PATTERN) final String keyVersion, + @RequestAttribute(name = ApiConstants.REQUEST_BASE_URI) final URI baseUri, + @Valid @RequestBody final KeyVerifyParameters request) { + log.info("Received request to {} verify using key: {} with version: {} using API version: {}", + baseUri.toString(), keyName, keyVersion, V_7_2); + + final ReadOnlyKeyVaultKeyEntity keyVaultKeyEntity = getEntityByNameAndVersion(baseUri, keyName, keyVersion); + final boolean result = keyVaultKeyEntity.verifySignedBytes(request.getDigestAsBase64DecodedBytes(), request.getAlgorithm(), + request.getValueAsBase64DecodedBytes()); + return ResponseEntity.ok(new KeyVerifyResult(result)); + } + @Override protected VersionedKeyEntityId versionedEntityId(final URI baseUri, final String name, final String version) { return new VersionedKeyEntityId(baseUri, name, version); diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java index e910d048..11f12d07 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverter.java @@ -57,8 +57,7 @@ private JsonWebKeyModel mapRsaFields(final ReadOnlyRsaKeyVaultKeyEntity entity) private JsonWebKeyModel mapEcFields(final ReadOnlyEcKeyVaultKeyEntity entity) { final JsonWebKeyModel jsonWebKeyModel = mapCommonKeyProperties(entity); jsonWebKeyModel.setCurveName(entity.getKeyCurveName()); - jsonWebKeyModel.setX(entity.getX()); - jsonWebKeyModel.setY(entity.getY()); + //skip mapping X and Y to not expose public key to client return jsonWebKeyModel; } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeySignResult.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeySignResult.java new file mode 100644 index 00000000..7d8b9d35 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeySignResult.java @@ -0,0 +1,36 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; +import lombok.Data; +import org.springframework.util.Assert; + +import java.net.URI; +import java.util.Base64; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KeySignResult { + + private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding(); + + @JsonProperty("kid") + private URI id; + @JsonProperty("value") + private String value; + + public static KeySignResult forBytes(@org.springframework.lang.NonNull final VersionedKeyEntityId keyEntityId, + @org.springframework.lang.NonNull final byte[] value) { + Assert.notNull(value, "Value must not be null."); + return forString(keyEntityId, ENCODER.encodeToString(value)); + } + + private static KeySignResult forString(final VersionedKeyEntityId keyEntityId, final String value) { + final KeySignResult result = new KeySignResult(); + result.setId(keyEntityId.asUri()); + result.setValue(value); + return result; + } + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyVerifyResult.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyVerifyResult.java new file mode 100644 index 00000000..46338063 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/KeyVerifyResult.java @@ -0,0 +1,18 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KeyVerifyResult { + + @JsonProperty("value") + private boolean value; + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithm.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithm.java new file mode 100644 index 00000000..63a0838f --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithm.java @@ -0,0 +1,79 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key.constants; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +@SuppressWarnings("checkstyle:JavadocVariable") +public enum SignatureAlgorithm { + + ES256("ES256", "NONEwithECDSA", KeyType.EC) { + @Override + public boolean isCompatibleWithCurve(final KeyCurveName keyCurveName) { + return KeyCurveName.P_256 == keyCurveName; + } + }, + ES256K("ES256K", "NONEwithECDSA", KeyType.EC) { + @Override + public boolean isCompatibleWithCurve(final KeyCurveName keyCurveName) { + return KeyCurveName.P_256K == keyCurveName; + } + }, + ES384("ES384", "NONEwithECDSA", KeyType.EC) { + @Override + public boolean isCompatibleWithCurve(final KeyCurveName keyCurveName) { + return KeyCurveName.P_384 == keyCurveName; + } + }, + ES512("ES512", "NONEwithECDSA", KeyType.EC) { + @Override + public boolean isCompatibleWithCurve(final KeyCurveName keyCurveName) { + return KeyCurveName.P_521 == keyCurveName; + } + }, + PS256("PS256", "SHA256withRSAandMGF1", KeyType.RSA), + PS384("PS384", "SHA384withRSAandMGF1", KeyType.RSA), + PS512("PS512", "SHA512withRSAandMGF1", KeyType.RSA), + RS256("RS256", "SHA256withRSA", KeyType.RSA), + RS384("RS384", "SHA384withRSA", KeyType.RSA), + RS512("RS512", "SHA512withRSA", KeyType.RSA); + + private final String value; + private final String alg; + private final KeyType compatibleType; + + SignatureAlgorithm(final String value, + final String alg, final KeyType compatibleType) { + this.value = value; + this.alg = alg; + this.compatibleType = compatibleType; + } + + @JsonCreator + public static SignatureAlgorithm forValue(final String name) { + return Arrays.stream(values()).filter(algorithm -> algorithm.getValue().equals(name)).findFirst().orElse(null); + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonIgnore + public String getAlg() { + return alg; + } + + @JsonIgnore + public boolean isCompatible(final KeyType type) { + return compatibleType == type; + } + + @JsonIgnore + public boolean isCompatibleWithCurve(final KeyCurveName keyCurveName) { + return false; + } + +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeySignParameters.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeySignParameters.java new file mode 100644 index 00000000..9b3e5027 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeySignParameters.java @@ -0,0 +1,35 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Base64; +import java.util.Optional; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KeySignParameters { + + private static final Base64.Decoder DECODER = Base64.getUrlDecoder(); + + @NotNull + @JsonProperty("alg") + private SignatureAlgorithm algorithm; + + @NotNull + @NotBlank + @JsonProperty("value") + private String value; + + @JsonIgnore + public byte[] getValueAsBase64DecodedBytes() { + return Optional.ofNullable(value) + .map(DECODER::decode) + .orElse(null); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeyVerifyParameters.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeyVerifyParameters.java new file mode 100644 index 00000000..36b37db0 --- /dev/null +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/request/KeyVerifyParameters.java @@ -0,0 +1,49 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Base64; +import java.util.Optional; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KeyVerifyParameters { + + private static final Base64.Decoder DECODER = Base64.getUrlDecoder(); + + @NotNull + @JsonProperty("alg") + private SignatureAlgorithm algorithm; + + @NotNull + @NotBlank + @JsonProperty("value") + private String value; + + @NotNull + @NotBlank + @JsonProperty("digest") + private String digest; + + @JsonIgnore + public byte[] getValueAsBase64DecodedBytes() { + return decodeOptionalStringAsBase64Bytes(value); + } + + @JsonIgnore + public byte[] getDigestAsBase64DecodedBytes() { + return decodeOptionalStringAsBase64Bytes(digest); + } + + private byte[] decodeOptionalStringAsBase64Bytes(final String digest) { + return Optional.ofNullable(digest) + .map(DECODER::decode) + .orElse(null); + } +} diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/ReadOnlyKeyVaultKeyEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/ReadOnlyKeyVaultKeyEntity.java index a735dc94..bd13408b 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/ReadOnlyKeyVaultKeyEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/ReadOnlyKeyVaultKeyEntity.java @@ -3,6 +3,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.common.BaseVaultEntity; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import org.springframework.util.Assert; @@ -28,6 +29,10 @@ default String decrypt(final byte[] encrypted, final EncryptionAlgorithm encrypt byte[] decryptToBytes(byte[] encrypted, EncryptionAlgorithm encryptionAlgorithm, byte[] iv); + byte[] signBytes(byte[] clear, SignatureAlgorithm encryptionAlgorithm); + + boolean verifySignedBytes(byte[] signed, SignatureAlgorithm encryptionAlgorithm, byte[] digest); + VersionedKeyEntityId getId(); URI getUri(); diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntity.java index 6e7cdf7d..209c94a3 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntity.java @@ -3,6 +3,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyAesKeyVaultKeyEntity; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; @@ -80,4 +81,16 @@ public byte[] decryptToBytes(@NonNull final byte[] encrypted, @NonNull final Enc return cipher.doFinal(encrypted); }, "Cannot decrypt message.", log); } + + @Override + public byte[] signBytes(final byte[] clear, final SignatureAlgorithm encryptionAlgorithm) { + throw new UnsupportedOperationException("Sign is not supported for OCT keys."); + } + + @Override + public boolean verifySignedBytes(final byte[] signed, + final SignatureAlgorithm encryptionAlgorithm, + final byte[] digest) { + throw new UnsupportedOperationException("Verify is not supported for OCT keys."); + } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntity.java index 0f4cd6e9..1b28c1b6 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntity.java @@ -1,15 +1,16 @@ package com.github.nagyesta.lowkeyvault.service.key.impl; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyCurveName; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.*; import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyEcKeyVaultKeyEntity; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.springframework.lang.NonNull; +import org.springframework.util.Assert; import java.security.KeyPair; +import java.security.Signature; import java.security.interfaces.ECPublicKey; @Slf4j @@ -60,4 +61,31 @@ public byte[] decryptToBytes(final byte[] encrypted, final EncryptionAlgorithm e throw new UnsupportedOperationException("Decrypt is not supported for EC keys."); } + @Override + public byte[] signBytes(final byte[] clear, final SignatureAlgorithm signatureAlgorithm) { + Assert.state(getOperations().contains(KeyOperation.SIGN), getId() + " does not have SIGN operation assigned."); + Assert.state(isEnabled(), getId() + " is not enabled."); + Assert.state(signatureAlgorithm.isCompatibleWithCurve(getKeyCurveName()), getId() + " is not using the right key curve."); + return doCrypto(() -> { + final Signature ecSign = Signature.getInstance(signatureAlgorithm.getAlg(), new BouncyCastleProvider()); + ecSign.initSign(getKey().getPrivate()); + ecSign.update(clear); + return ecSign.sign(); + }, "Cannot sign message.", log); + } + + @Override + public boolean verifySignedBytes(final byte[] signed, + final SignatureAlgorithm signatureAlgorithm, + final byte[] digest) { + Assert.state(getOperations().contains(KeyOperation.VERIFY), getId() + " does not have VERIFY operation assigned."); + Assert.state(isEnabled(), getId() + " is not enabled."); + Assert.state(signatureAlgorithm.isCompatibleWithCurve(getKeyCurveName()), getId() + " is not using the right key curve."); + return doCrypto(() -> { + final Signature ecVerify = Signature.getInstance(signatureAlgorithm.getAlg(), new BouncyCastleProvider()); + ecVerify.initVerify(getKey().getPublic()); + ecVerify.update(signed); + return ecVerify.verify(digest); + }, "Cannot verify signed message.", log); + } } diff --git a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java index 4f0e3546..3d8d3e21 100644 --- a/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java +++ b/lowkey-vault-app/src/main/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntity.java @@ -3,6 +3,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyRsaKeyVaultKeyEntity; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; @@ -14,6 +15,7 @@ import javax.crypto.Cipher; import java.math.BigInteger; import java.security.KeyPair; +import java.security.Signature; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAKeyGenParameterSpec; import java.util.Objects; @@ -83,4 +85,30 @@ public byte[] decryptToBytes(@NonNull final byte[] encrypted, @NonNull final Enc return cipher.doFinal(encrypted); }, "Cannot decrypt message.", log); } + + @Override + public byte[] signBytes(final byte[] clear, final SignatureAlgorithm signatureAlgorithm) { + Assert.state(getOperations().contains(KeyOperation.SIGN), getId() + " does not have SIGN operation assigned."); + Assert.state(isEnabled(), getId() + " is not enabled."); + return doCrypto(() -> { + final Signature rsaSign = Signature.getInstance(signatureAlgorithm.getAlg(), new BouncyCastleProvider()); + rsaSign.initSign(getKey().getPrivate()); + rsaSign.update(clear); + return rsaSign.sign(); + }, "Cannot sign message.", log); + } + + @Override + public boolean verifySignedBytes(final byte[] signed, + final SignatureAlgorithm signatureAlgorithm, + final byte[] digest) { + Assert.state(getOperations().contains(KeyOperation.VERIFY), getId() + " does not have VERIFY operation assigned."); + Assert.state(isEnabled(), getId() + " is not enabled."); + return doCrypto(() -> { + final Signature rsaVerify = Signature.getInstance(signatureAlgorithm.getAlg(), new BouncyCastleProvider()); + rsaVerify.initVerify(getKey().getPublic()); + rsaVerify.update(signed); + return rsaVerify.verify(digest); + }, "Cannot verify signed message.", log); + } } diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyControllerTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyControllerTest.java index dfce2143..946e0c01 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyControllerTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/controller/v7_2/KeyControllerTest.java @@ -12,9 +12,8 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.CreateKeyRequest; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.KeyOperationsParameters; -import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.UpdateKeyRequest; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.request.*; import com.github.nagyesta.lowkeyvault.service.common.ReadOnlyVersionedEntityMultiMap; import com.github.nagyesta.lowkeyvault.service.exception.AlreadyExistsException; import com.github.nagyesta.lowkeyvault.service.exception.CryptoException; @@ -892,6 +891,55 @@ void testEncryptAndDecryptShouldGetBackOriginalInputWhenKeyAndVersionIsFound(fin verify(entities, times(2)).getReadOnlyEntity(eq(VERSIONED_KEY_ENTITY_ID_1_VERSION_3)); } + @SuppressWarnings("checkstyle:MagicNumber") + @ParameterizedTest + @ValueSource(strings = {BLANK, DEFAULT_VAULT, LOCALHOST, LOWKEY_VAULT}) + void testSignAndVerifyShouldReturnTrueWhenKeyAndVersionIsFoundAndCalledInSequence(final String clearText) { + //given + final KeyEntityId baseUri = new KeyEntityId(HTTPS_LOCALHOST_8443, KEY_NAME_1, null); + final List operations = List.of(KeyOperation.SIGN, KeyOperation.VERIFY); + final CreateKeyRequest request = createRequest(operations, null, null); + final RsaKeyVaultKeyEntity entity = (RsaKeyVaultKeyEntity) createEntity(VERSIONED_KEY_ENTITY_ID_1_VERSION_1, request); + entity.setEnabled(true); + entity.setOperations(operations); + when(keyVaultFake.getEntities()) + .thenReturn(entities); + when(entities.getReadOnlyEntity(eq(VERSIONED_KEY_ENTITY_ID_1_VERSION_3))) + .thenReturn(entity); + when(keyEntityToV72ModelConverter.convert(same(entity))) + .thenReturn(RESPONSE); + final KeySignParameters keySignParameters = new KeySignParameters(); + keySignParameters.setAlgorithm(SignatureAlgorithm.PS256); + keySignParameters.setValue(ENCODER.encodeToString(clearText.getBytes(StandardCharsets.UTF_8))); + + //when + final ResponseEntity signature = underTest + .sign(KEY_NAME_1, KEY_VERSION_3, HTTPS_LOCALHOST_8443, keySignParameters); + Assertions.assertNotNull(signature); + Assertions.assertEquals(HttpStatus.OK, signature.getStatusCode()); + Assertions.assertNotNull(signature.getBody()); + Assertions.assertNotEquals(clearText, signature.getBody().getValue()); + + final KeyVerifyParameters verifyParameters = new KeyVerifyParameters(); + verifyParameters.setAlgorithm(SignatureAlgorithm.PS256); + verifyParameters.setDigest(ENCODER.encodeToString(clearText.getBytes(StandardCharsets.UTF_8))); + verifyParameters.setValue(signature.getBody().getValue()); + final ResponseEntity actual = underTest + .verify(KEY_NAME_1, KEY_VERSION_3, HTTPS_LOCALHOST_8443, verifyParameters); + + //then + Assertions.assertNotNull(actual); + Assertions.assertEquals(HttpStatus.OK, actual.getStatusCode()); + Assertions.assertNotNull(actual.getBody()); + Assertions.assertTrue(actual.getBody().isValue()); + + verify(vaultService, times(2)).findByUri(eq(HTTPS_LOCALHOST_8443)); + verify(vaultFake, times(2)).keyVaultFake(); + verify(keyVaultFake, times(2)).getEntities(); + verify(entities, never()).getLatestVersionOfEntity(eq(baseUri)); + verify(entities, times(2)).getReadOnlyEntity(eq(VERSIONED_KEY_ENTITY_ID_1_VERSION_3)); + } + @NonNull private CreateKeyRequest createRequest( final List operations, diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java index d79e5e28..a8761c56 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72ModelConverterTest.java @@ -143,8 +143,6 @@ void testConvertShouldConvertAllEcFieldsWhenCalledWithEcKeyEntity( Assertions.assertEquals(KeyType.EC, actual.getKey().getKeyType()); } Assertions.assertEquals(input.getKeyCurveName(), actual.getKey().getCurveName()); - Assertions.assertArrayEquals(input.getX(), actual.getKey().getX()); - Assertions.assertArrayEquals(input.getY(), actual.getKey().getY()); assertRsaFieldsAreNull(actual); assertOctFieldsAreNull(actual); diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72PropertiesModelConverterTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72PropertiesModelConverterTest.java index 025f719b..6e234366 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72PropertiesModelConverterTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/mapper/v7_2/key/KeyEntityToV72PropertiesModelConverterTest.java @@ -4,6 +4,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.KeyPropertiesModel; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.KeyVaultFake; import com.github.nagyesta.lowkeyvault.service.key.impl.KeyVaultKeyEntity; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; @@ -136,5 +137,17 @@ public RecoveryLevel getRecoveryLevel() { public void setRecoveryLevel(final RecoveryLevel recoveryLevel) { this.recoveryLevel = recoveryLevel; } + + @Override + public byte[] signBytes(final byte[] clear, final SignatureAlgorithm encryptionAlgorithm) { + return new byte[0]; + } + + @Override + public boolean verifySignedBytes(final byte[] signed, + final SignatureAlgorithm encryptionAlgorithm, + final byte[] digest) { + return false; + } } } diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithmTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithmTest.java new file mode 100644 index 00000000..6fca2ae4 --- /dev/null +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/model/v7_2/key/constants/SignatureAlgorithmTest.java @@ -0,0 +1,46 @@ +package com.github.nagyesta.lowkeyvault.model.v7_2.key.constants; + +import com.github.nagyesta.lowkeyvault.TestConstants; +import org.junit.jupiter.api.Assertions; +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 java.util.Arrays; +import java.util.stream.Stream; + +class SignatureAlgorithmTest { + + public static Stream valueProvider() { + final Stream.Builder builder = Stream.builder() + .add(Arguments.of(null, null)) + .add(Arguments.of(TestConstants.EMPTY, null)); + Arrays.stream(SignatureAlgorithm.values()).forEach(a -> builder.add(Arguments.of(a.getValue(), a))); + return builder.build(); + } + + @Test + void testIsCompatibleWithCurveShouldReturnFalseInCaseOfRsaAlgorithm() { + //given + final SignatureAlgorithm underTest = SignatureAlgorithm.PS256; + + //when + final boolean actual = underTest.isCompatibleWithCurve(KeyCurveName.P_256); + + //then + Assertions.assertFalse(actual); + } + + @ParameterizedTest + @MethodSource("valueProvider") + void forValue(final String inout, final SignatureAlgorithm expected) { + //given + + //when + final SignatureAlgorithm actual = SignatureAlgorithm.forValue(inout); + + //then + Assertions.assertEquals(expected, actual); + } +} diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntityTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntityTest.java index 112c3753..664e1166 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntityTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/AesKeyVaultKeyEntityTest.java @@ -3,6 +3,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; import com.github.nagyesta.lowkeyvault.service.vault.impl.VaultFakeImpl; @@ -210,4 +211,37 @@ void testDecryptShouldThrowExceptionWhenKeyIsNotEnabled() { //then + exception } + + @Test + void testSignShouldThrowExceptionWhenCalled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final AesKeyVaultKeyEntity underTest = new AesKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyType.OCT.getValidKeyParameters(Integer.class).first(), false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + Assertions.assertThrows(UnsupportedOperationException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenCalled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final AesKeyVaultKeyEntity underTest = new AesKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyType.OCT.getValidKeyParameters(Integer.class).first(), false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + Assertions.assertThrows(UnsupportedOperationException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), + SignatureAlgorithm.ES256, DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8))); + + //then + exception + } } diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntityTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntityTest.java index 6cf74c0e..f3ff22ac 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntityTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/EcKeyVaultKeyEntityTest.java @@ -1,6 +1,9 @@ package com.github.nagyesta.lowkeyvault.service.key.impl; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyCurveName; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; import com.github.nagyesta.lowkeyvault.service.vault.impl.VaultFakeImpl; @@ -10,9 +13,12 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; import java.util.stream.Stream; -import static com.github.nagyesta.lowkeyvault.TestConstants.DEFAULT_VAULT; +import static com.github.nagyesta.lowkeyvault.TestConstants.*; import static com.github.nagyesta.lowkeyvault.TestConstantsKeys.VERSIONED_KEY_ENTITY_ID_1_VERSION_1; import static com.github.nagyesta.lowkeyvault.TestConstantsUri.HTTPS_LOWKEY_VAULT; import static org.mockito.Mockito.mock; @@ -31,6 +37,21 @@ public static Stream invalidValueProvider() { .build(); } + public static Stream stringSignSource() { + return Arrays.stream(SignatureAlgorithm.values()) + .filter(sa -> sa.isCompatible(KeyType.EC)) + .flatMap(sa -> Arrays.stream(KeyCurveName.values()) + .filter(sa::isCompatibleWithCurve) + .flatMap(kcn -> Stream.builder() + .add(Arguments.of(DEFAULT_VAULT, DEFAULT_VAULT, sa, kcn)) + .add(Arguments.of(LOCALHOST, LOCALHOST, sa, kcn)) + .add(Arguments.of(LOWKEY_VAULT, LOWKEY_VAULT, sa, kcn)) + .add(Arguments.of(DEFAULT_VAULT, LOCALHOST, sa, kcn)) + .add(Arguments.of(LOCALHOST, DEFAULT_VAULT, sa, kcn)) + .add(Arguments.of(LOWKEY_VAULT, LOCALHOST, sa, kcn)) + .build())); + } + @ParameterizedTest @MethodSource("invalidValueProvider") void testConstructorShouldThrowExceptionWhenCalledWithNull( @@ -58,4 +79,123 @@ void testEncryptThenDecryptShouldBothThrowExceptionsWhenCalled() { //then + exception } + + @ParameterizedTest + @MethodSource("stringSignSource") + void testSignThenVerifyShouldReturnTrueWhenVerificationAndSignAreUsingTheSameData( + final String clearSign, final String clearVerify, final SignatureAlgorithm algorithm, final KeyCurveName keyCurveName) { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, keyCurveName, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(clearSign.getBytes(StandardCharsets.UTF_8), algorithm); + final boolean actual = underTest.verifySignedBytes(clearVerify.getBytes(StandardCharsets.UTF_8), algorithm, signature); + + //then + Assertions.assertEquals(clearSign.equals(clearVerify), actual); + } + + @Test + void testSignShouldThrowExceptionWhenOperationIsNotAllowed() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenOperationIsNotAllowed() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.SIGN)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256); + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256, signature)); + + //then + exception + } + + @Test + void testSignShouldThrowExceptionWhenOperationIsNotEnabled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(false); + + //when + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenKeyIsNotEnabled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256); + underTest.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256, signature)); + + //then + exception + } + + @Test + void testSignShouldThrowExceptionWhenKeyCurveIsNotCompatible() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256K)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenWhenKeyCurveIsNotCompatible() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final EcKeyVaultKeyEntity underTest = new EcKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, KeyCurveName.P_256, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256); + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.ES256K, signature)); + + //then + exception + } } diff --git a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntityTest.java b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntityTest.java index 88fb0a79..df0b6e79 100644 --- a/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntityTest.java +++ b/lowkey-vault-app/src/test/java/com/github/nagyesta/lowkeyvault/service/key/impl/RsaKeyVaultKeyEntityTest.java @@ -3,6 +3,7 @@ import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.EncryptionAlgorithm; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyOperation; import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.KeyType; +import com.github.nagyesta.lowkeyvault.model.v7_2.key.constants.SignatureAlgorithm; import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId; import com.github.nagyesta.lowkeyvault.service.vault.VaultFake; import com.github.nagyesta.lowkeyvault.service.vault.impl.VaultFakeImpl; @@ -13,6 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; @@ -46,6 +48,20 @@ public static Stream stringSource() { .build()); } + @SuppressWarnings("checkstyle:MagicNumber") + public static Stream stringSignSource() { + return Arrays.stream(SignatureAlgorithm.values()) + .filter(sa -> sa.isCompatible(KeyType.RSA)) + .flatMap(sa -> Stream.builder() + .add(Arguments.of(DEFAULT_VAULT, DEFAULT_VAULT, sa, 2048)) + .add(Arguments.of(LOCALHOST, LOCALHOST, sa, 3072)) + .add(Arguments.of(LOWKEY_VAULT, LOWKEY_VAULT, sa, 4096)) + .add(Arguments.of(DEFAULT_VAULT, LOCALHOST, sa, 2048)) + .add(Arguments.of(LOCALHOST, DEFAULT_VAULT, sa, 3072)) + .add(Arguments.of(LOWKEY_VAULT, LOCALHOST, sa, 4096)) + .build()); + } + @ParameterizedTest @MethodSource("invalidValueProvider") void testConstructorShouldThrowExceptionWhenCalledWithNull( @@ -143,4 +159,90 @@ void testDecryptShouldThrowExceptionWhenKeyIsNotEnabled() { //then + exception } + + @ParameterizedTest + @MethodSource("stringSignSource") + void testSignThenVerifyShouldReturnTrueWhenVerificationAndSignAreUsingTheSameData( + final String clearSign, final String clearVerify, final SignatureAlgorithm algorithm, final int keySize) { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final RsaKeyVaultKeyEntity underTest = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, keySize, null, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(clearSign.getBytes(StandardCharsets.UTF_8), algorithm); + final boolean actual = underTest.verifySignedBytes(clearVerify.getBytes(StandardCharsets.UTF_8), algorithm, signature); + + //then + Assertions.assertEquals(clearSign.equals(clearVerify), actual); + } + + @Test + void testSignShouldThrowExceptionWhenOperationIsNotAllowed() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final RsaKeyVaultKeyEntity underTest = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, EncryptionAlgorithm.RSA_OAEP_256.getMinKeySize(), null, false); + underTest.setOperations(List.of(KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenOperationIsNotAllowed() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final RsaKeyVaultKeyEntity underTest = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, EncryptionAlgorithm.RSA_OAEP_256.getMinKeySize(), null, false); + underTest.setOperations(List.of(KeyOperation.SIGN)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256); + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256, signature)); + + //then + exception + } + + @Test + void testSignShouldThrowExceptionWhenOperationIsNotEnabled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final RsaKeyVaultKeyEntity underTest = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, EncryptionAlgorithm.RSA_OAEP_256.getMinKeySize(), null, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(false); + + //when + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256)); + + //then + exception + } + + @Test + void testVerifyShouldThrowExceptionWhenKeyIsNotEnabled() { + //given + final VaultFake vaultFake = new VaultFakeImpl(HTTPS_LOWKEY_VAULT); + final RsaKeyVaultKeyEntity underTest = new RsaKeyVaultKeyEntity( + VERSIONED_KEY_ENTITY_ID_1_VERSION_1, vaultFake, EncryptionAlgorithm.RSA_OAEP_256.getMinKeySize(), null, false); + underTest.setOperations(List.of(KeyOperation.SIGN, KeyOperation.VERIFY)); + underTest.setEnabled(true); + + //when + final byte[] signature = underTest.signBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256); + underTest.setEnabled(false); + Assertions.assertThrows(IllegalStateException.class, + () -> underTest.verifySignedBytes(DEFAULT_VAULT.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.PS256, signature)); + + //then + exception + } } diff --git a/lowkey-vault-docker/src/docker/Dockerfile b/lowkey-vault-docker/src/docker/Dockerfile index 3d38616b..58c1412a 100644 --- a/lowkey-vault-docker/src/docker/Dockerfile +++ b/lowkey-vault-docker/src/docker/Dockerfile @@ -1,4 +1,5 @@ -FROM mcr.microsoft.com/java/jre-headless:11-zulu-alpine +#openjdk:11.0.14-jre-slim-buster amd64 +FROM openjdk@sha256:98f19a6e8f22ab752fe98c2c5e4c100d2f5a32eedfd4ac5b8adb9bd3a390c53e LABEL maintainer="nagyesta@gmail.com" EXPOSE 8443:8443 ADD lowkey-vault.jar /lowkey-vault.jar diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/MissionOutlineDefinition.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/MissionOutlineDefinition.java index cbfb7c74..8a4aa122 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/MissionOutlineDefinition.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/MissionOutlineDefinition.java @@ -47,7 +47,7 @@ protected Map> defineOutline() { }); }); - Stream.of("KeyEncrypt", "RSA", "EC", "OCT").forEach(tag -> { + Stream.of("KeyEncrypt", "KeySign", "RSA", "EC", "OCT").forEach(tag -> { final MissionHealthCheckMatcher matcher = matcher().dependencyWith(tag).extractor(extractor).build(); final MissionHealthCheckEvaluator tagPercentage = percentageBasedEvaluator(matcher) .abortThreshold(ABORT_THRESHOLD) diff --git a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/KeyTestContext.java b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/KeyTestContext.java index a540d50d..1bc5e709 100644 --- a/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/KeyTestContext.java +++ b/lowkey-vault-docker/src/test/java/com/github/nagyesta/lowkeyvault/context/KeyTestContext.java @@ -22,6 +22,8 @@ public class KeyTestContext extends CommonTestContext is prepared with bits size without HSM + And the key has sign,verify operations granted + And the RSA key is created + When the created key is used to sign with + And the signed value is not + And the signature of is verified with + Then the signature matches + + Examples: + | keyName | keySize | algorithm | clearText | + | signRsaKey-1 | 2048 | PS256 | The quick brown fox jumps over the lazy dog. | + | signRsaKey-2 | 2048 | PS384 | | + | signRsaKey-3 | 2048 | PS512 | The quick brown fox jumps over the lazy dog. | + | signRsaKey-4 | 4096 | RS256 | The quick brown fox jumps over the lazy dog. | + | signRsaKey-5 | 4096 | RS384 | | + | signRsaKey-6 | 4096 | RS512 | The quick brown fox jumps over the lazy dog. | + + @Key @KeyCreate @KeySign @EC + Scenario Outline: EC_SIGN_01 An EC key is created with the key client then used for sign and verify operations + Given a key client is created with the vault named keys-generic + And an EC key named is prepared with and without HSM + And the key has sign,verify operations granted + And the EC key is created + When the created key is used to sign with + And the signed value is not + And the signature of is verified with + Then the signature matches + + Examples: + | keyName | curveName | algorithm | clearText | + | signEc-1 | P-256 | ES256 | The quick brown fox jumps over the lazy dog. | + | signEc-2 | P-256K | ES256K | The quick brown fox jumps over the lazy dog. | + | signEc-3 | P-384 | ES384 | The quick brown fox jumps over the lazy dog. | + | signEc-4 | P-521 | ES512 | The quick brown fox jumps over the lazy dog. | + | signEc-5 | P-256 | ES256 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | signEc-6 | P-256K | ES256K | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | signEc-7 | P-384 | ES384 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. | + | signEc-8 | P-521 | ES512 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do. |