Skip to content

Commit

Permalink
Key feature - Sign/Verify (#19)
Browse files Browse the repository at this point in the history
- Updates token permissions of CI workflows
- Updates CodeQL action version
- Swaps to official OpenJDK image and pins down version
- Adds Sign/Verify support

Resolves #18
{minor}
  • Loading branch information
nagyesta authored Feb 12, 2022
1 parent e293c95 commit 13f3426
Show file tree
Hide file tree
Showing 31 changed files with 866 additions and 21 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ on:
- '.github/ISSUE_TEMPLATE/**'
- '.github/assets/**'

permissions: read-all

jobs:
analyze:
name: Analyze
Expand All @@ -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.
Expand All @@ -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 }}
2 changes: 2 additions & 0 deletions .github/workflows/gradle-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ on:
- '.github/ISSUE_TEMPLATE/**'
- '.github/assets/**'

permissions: read-all

jobs:
build:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/gradle-oss-index-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ on:
- '.github/ISSUE_TEMPLATE/**'
- '.github/assets/**'

permissions: read-all

jobs:
build:
runs-on: ubuntu-latest
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions lowkey-vault-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -238,6 +236,41 @@ public ResponseEntity<KeyOperationsResult> 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<KeySignResult> 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<KeyVerifyResult> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
Loading

0 comments on commit 13f3426

Please sign in to comment.