Skip to content

Commit

Permalink
Certificate API - Lifetime action policy execution (#499)
Browse files Browse the repository at this point in the history
- Adds new parameter to time shift endpoints to allow regeneration of certificates
- Regenerates certificates if time shift requires it
- Performs automatic certificate renewals during time shift based on lifetime actions
- Rotate non-reusable keys during automatic certificate renewals
- Updates timestamps of backing keys/secrets based on calculated validity start/end of certificate
- Updates client to support new time shift parameter
- Covers new functionality with UT/IT
- Adds new end-2-end tests
- Updates documentation

Resolves #481
{minor}

Signed-off-by: Esta Nagy <[email protected]>
  • Loading branch information
nagyesta authored Mar 13, 2023
1 parent 26c1139 commit 0ca6e6d
Show file tree
Hide file tree
Showing 35 changed files with 769 additions and 68 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,7 @@ Lowkey Vault is far from supporting all Azure Key Vault features. The list suppo
Certificate API features are work in progress, many Lowkey Vault features might
not work or are known to be broken, for example but not limited to the following:

- Certificate lifetime policy does not have any effect
- Import and export ignores certificates
- Time shift is not supported for certificates

### Management API

Expand Down Expand Up @@ -220,5 +218,6 @@ not work or are known to be broken, for example but not limited to the following
# Limitations

- Some encryption/signature algorithms are not supported. Please refer to the ["Features"](#features) section for the up-to-date list of supported algorithms.
- Certificate Vault features are not supported at the moment
- Only self-signed certificates are supported by the certificate API.
- Time shift cannot renew/recreate deleted certificates. Please consider performing deletions after time shift as a work around.
- Recovery options cannot be configured for vaults created during start-up
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,17 @@ public ResponseEntity<VaultModel> aliasUpdate(@RequestParam final URI baseUri,
schema = @Schema(implementation = ErrorModel.class)
))},
parameters = {
@Parameter(name = "seconds", example = ONE, description = "The number of seconds we want to shift.")},
@Parameter(name = "seconds", example = ONE, description = "The number of seconds we want to shift."),
@Parameter(name = "regenerateCertificates", example = FALSE,
description = "Whether we allow regeneration of certificates to let their validity match the new time-frame.")},
requestBody = @RequestBody(content = @Content(mediaType = MimeTypeUtils.APPLICATION_JSON_VALUE)))
@PutMapping(value = "/time/all", params = {"seconds"})
public ResponseEntity<Void> timeShiftAll(@RequestParam final int seconds) {
log.info("Received request to shift time of ALL vaults by {} seconds.", seconds);
vaultService.timeShift(seconds);
public ResponseEntity<Void> timeShiftAll(
@RequestParam final int seconds,
@RequestParam(required = false, defaultValue = "false") final boolean regenerateCertificates) {
log.info("Received request to shift time of ALL vaults by {} seconds, regenerate certificates: {}.",
seconds, regenerateCertificates);
vaultService.timeShift(seconds, regenerateCertificates);
return ResponseEntity.noContent().build();
}

Expand Down Expand Up @@ -319,12 +324,17 @@ public ResponseEntity<Void> timeShiftAll(@RequestParam final int seconds) {
))},
parameters = {
@Parameter(name = "seconds", example = ONE, description = "The number of seconds we want to shift."),
@Parameter(name = "baseUri", example = BASE_URI, description = "The base URI of the vault we want to shift.")},
@Parameter(name = "baseUri", example = BASE_URI, description = "The base URI of the vault we want to shift."),
@Parameter(name = "regenerateCertificates", example = FALSE,
description = "Whether we allow regeneration of certificates to let their validity match the new time-frame.")},
requestBody = @RequestBody(content = @Content(mediaType = MimeTypeUtils.APPLICATION_JSON_VALUE)))
@PutMapping(value = "/time", params = {"baseUri", "seconds"})
public ResponseEntity<Void> timeShiftSingle(@RequestParam final URI baseUri, @RequestParam final int seconds) {
log.info("Received request to shift time of vault with uri: {} by {} seconds.", baseUri, seconds);
vaultService.findByUriIncludeDeleted(baseUri).timeShift(seconds);
public ResponseEntity<Void> timeShiftSingle(
@RequestParam final URI baseUri, @RequestParam final int seconds,
@RequestParam(required = false, defaultValue = "false") final boolean regenerateCertificates) {
log.info("Received request to shift time of vault with uri: {}, by {} seconds, regenerate certificates: {}.",
baseUri, seconds, regenerateCertificates);
vaultService.findByUriIncludeDeleted(baseUri).timeShift(seconds, regenerateCertificates);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ public final class Examples {
* Example alias base URIs in a collection.
*/
public static final String ALIASES = "[\"" + ALIAS1 + "\",\"" + ALIAS2 + "\"]";
/**
* The literal false as a String.
*/
public static final String FALSE = "false";
/**
* The literal 1 as a String.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public interface CertificateVaultFake
LifetimeActionPolicy lifetimeActionPolicy(@NonNull CertificateEntityId certificateEntityId);

void setLifetimeActionPolicy(@NonNull LifetimeActionPolicy lifetimeActionPolicy);

void regenerateCertificates();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public interface ReadOnlyLifetimeActionPolicy {

Expand All @@ -20,5 +21,5 @@ public interface ReadOnlyLifetimeActionPolicy {

void validate(int validityMonths);

List<OffsetDateTime> missedRenewalDays(OffsetDateTime validityStart, OffsetDateTime expiry);
List<OffsetDateTime> missedRenewalDays(OffsetDateTime validityStart, Function<OffsetDateTime, OffsetDateTime> createdToExpiryFunction);
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public CertificateGenerator(@NonNull final VaultFake vault, @NonNull final Versi
}

public X509Certificate generateCertificate(
@NonNull final CertificateCreationInput input) throws CryptoException {
@NonNull final ReadOnlyCertificatePolicy input) throws CryptoException {
try {
final ReadOnlyAsymmetricKeyVaultKeyEntity readOnlyKeyVaultKey = vault.keyVaultFake().getEntities()
.getEntity(kid, ReadOnlyAsymmetricKeyVaultKeyEntity.class);
Expand Down Expand Up @@ -94,7 +94,7 @@ public PKCS10CertificationRequest generateCertificateSigningRequest(
}
}

private X509Certificate generateCertificate(final CertificateCreationInput input, final KeyPair keyPair)
private X509Certificate generateCertificate(final ReadOnlyCertificatePolicy input, final KeyPair keyPair)
throws IOException, OperatorCreationException, CertificateException {

final X509v3CertificateBuilder builder = createCertificateBuilder(input, keyPair);
Expand Down Expand Up @@ -130,7 +130,7 @@ private ExtendedKeyUsage convertUsageExtensions(final ReadOnlyCertificatePolicy
}

private X509v3CertificateBuilder createCertificateBuilder(
final CertificateCreationInput input, final KeyPair keyPair) throws IOException {
final ReadOnlyCertificatePolicy input, final KeyPair keyPair) throws IOException {
final X500Name subject = generateSubject(input.getSubject());
final X509v3CertificateBuilder certificate = new JcaX509v3CertificateBuilder(
subject, generateSerial(), input.certNotBefore(), input.certExpiry(), subject, keyPair.getPublic());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class CertificateLifetimeActionPolicy extends BaseLifetimePolicy<CertificateEntityId> implements LifetimeActionPolicy {

Expand Down Expand Up @@ -45,12 +46,14 @@ public void validate(final int validityMonths) {
}

@Override
public List<OffsetDateTime> missedRenewalDays(final OffsetDateTime validityStart, final OffsetDateTime expiry) {
public List<OffsetDateTime> missedRenewalDays(final OffsetDateTime validityStart,
final Function<OffsetDateTime, OffsetDateTime> createdToExpiryFunction) {
Assert.isTrue(isAutoRenew(), "Cannot have missed renewals without an \"AutoRenew\" lifetime action.");
final long triggersAfterDays = lifetimeActions.get(CertificateLifetimeActionActivity.AUTO_RENEW)
.triggersAfterDays(validityStart, expiry);
final OffsetDateTime startPoint = findTriggerTimeOffset(validityStart, triggersAfterDays);
return collectMissedTriggerDays(triggersAfterDays, startPoint);
final CertificateLifetimeActionTrigger trigger = lifetimeActions.get(CertificateLifetimeActionActivity.AUTO_RENEW);
final Function<OffsetDateTime, Long> triggerAfterDaysFunction = s -> trigger
.triggersAfterDays(s, createdToExpiryFunction.apply(s));
final OffsetDateTime startPoint = findTriggerTimeOffset(validityStart, triggerAfterDaysFunction);
return collectMissedTriggerDays(triggerAfterDaysFunction, startPoint);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
import com.github.nagyesta.lowkeyvault.service.certificate.ReadOnlyKeyVaultCertificateEntity;
import com.github.nagyesta.lowkeyvault.service.certificate.id.CertificateEntityId;
import com.github.nagyesta.lowkeyvault.service.certificate.id.VersionedCertificateEntityId;
import com.github.nagyesta.lowkeyvault.service.common.ReadOnlyVersionedEntityMultiMap;
import com.github.nagyesta.lowkeyvault.service.common.impl.BaseVaultFakeImpl;
import com.github.nagyesta.lowkeyvault.service.key.ReadOnlyKeyVaultKeyEntity;
import com.github.nagyesta.lowkeyvault.service.key.id.KeyEntityId;
import com.github.nagyesta.lowkeyvault.service.key.id.VersionedKeyEntityId;
import com.github.nagyesta.lowkeyvault.service.key.impl.KeyVaultKeyEntity;
import com.github.nagyesta.lowkeyvault.service.secret.id.SecretEntityId;
import com.github.nagyesta.lowkeyvault.service.vault.VaultFake;
import lombok.NonNull;

import java.time.OffsetDateTime;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

public class CertificateVaultFakeImpl
extends BaseVaultFakeImpl<CertificateEntityId, VersionedCertificateEntityId,
Expand Down Expand Up @@ -48,6 +54,74 @@ public VersionedCertificateEntityId importCertificateVersion(
return addVersion(entity.getId(), entity);
}

@Override
public void timeShift(final int offsetSeconds) {
super.timeShift(offsetSeconds);
lifetimeActionPolicies.values().forEach(p -> p.timeShift(offsetSeconds));
performPastRenewals();
}

private void performPastRenewals() {
purgeDeletedPolicies();
lifetimeActionPolicies.values().stream()
.filter(LifetimeActionPolicy::isAutoRenew)
.filter(l -> getEntities().containsEntity(l.getId()))
.forEach(this::performMissedRenewalsOfPolicy);
}

private void performMissedRenewalsOfPolicy(final LifetimeActionPolicy lifetimeActionPolicy) {
final CertificateEntityId certificateEntityId = lifetimeActionPolicy.getId();
final VersionedCertificateEntityId latestVersionOfEntity = getEntities().getLatestVersionOfEntity(certificateEntityId);
final ReadOnlyKeyVaultCertificateEntity readOnlyEntity = getEntities().getReadOnlyEntity(latestVersionOfEntity);
final Function<OffsetDateTime, OffsetDateTime> createdToExpiryFunction = s -> s
.plusMonths(readOnlyEntity.getPolicy().getValidityMonths());
lifetimeActionPolicy.missedRenewalDays(readOnlyEntity.getCreated(), createdToExpiryFunction)
.forEach(renewalTime -> simulatePointInTimeRotation(certificateEntityId, renewalTime));
}

private void simulatePointInTimeRotation(final CertificateEntityId certificateEntityId, final OffsetDateTime renewalTime) {
final ReadOnlyKeyVaultCertificateEntity latest = latestReadOnlyCertificateVersion(certificateEntityId);
final CertificatePolicy input = new CertificatePolicy(latest.getPolicy());
input.setValidityStart(renewalTime);
final VersionedKeyEntityId kid = rotateIfNeededAndGetLastKeyId(input);
final VersionedCertificateEntityId id = generateIdOfNewCertificateEntity(input, kid);
final KeyVaultCertificateEntity entity = new KeyVaultCertificateEntity(input, kid, id, vaultFake());
addVersion(entity.getId(), entity);
}

private VersionedCertificateEntityId generateIdOfNewCertificateEntity(
final ReadOnlyCertificatePolicy input, final VersionedKeyEntityId kid) {
final VersionedCertificateEntityId id;
if (input.isReuseKeyOnRenewal()) {
id = new VersionedCertificateEntityId(vaultFake().baseUri(), input.getName());
} else {
id = new VersionedCertificateEntityId(vaultFake().baseUri(), input.getName(), kid.version());
}
return id;
}

private VersionedKeyEntityId rotateIfNeededAndGetLastKeyId(final ReadOnlyCertificatePolicy input) {
final ReadOnlyVersionedEntityMultiMap<KeyEntityId, VersionedKeyEntityId, ReadOnlyKeyVaultKeyEntity> entities = vaultFake()
.keyVaultFake().getEntities();
final VersionedKeyEntityId versionedKeyEntityId;
if (input.isReuseKeyOnRenewal()) {
final String lastVersion = entities.getVersions(new KeyEntityId(vaultFake().baseUri(), input.getName())).getLast();
versionedKeyEntityId = new VersionedKeyEntityId(vaultFake().baseUri(), input.getName(), lastVersion);
} else {
versionedKeyEntityId = vaultFake().keyVaultFake().rotateKey(new KeyEntityId(vaultFake().baseUri(), input.getName()));
//update timestamps
final OffsetDateTime notBefore = input.getValidityStart();
final OffsetDateTime expiry = notBefore.plusMonths(input.getValidityMonths());
vaultFake().keyVaultFake().setExpiry(versionedKeyEntityId, notBefore, expiry);
final KeyVaultKeyEntity<?, ?> entity = vaultFake()
.keyVaultFake().getEntities().getEntity(versionedKeyEntityId, KeyVaultKeyEntity.class);
entity.setManaged(true);
entity.setCreatedOn(notBefore);
entity.setUpdatedOn(notBefore);
}
return versionedKeyEntityId;
}

@Override
public void delete(@NonNull final CertificateEntityId entityId) {
super.delete(entityId);
Expand Down Expand Up @@ -87,6 +161,12 @@ public void setLifetimeActionPolicy(@NonNull final LifetimeActionPolicy lifetime
}
}

@Override
public void regenerateCertificates() {
this.getEntitiesInternal().forEachEntity(entity -> entity.regenerateCertificate(this.vaultFake()));
this.getDeletedEntitiesInternal().forEachEntity(entity -> entity.regenerateCertificate(this.vaultFake()));
}

private void purgeDeletedPolicies() {
keepNamesReadyForRemoval(lifetimeActionPolicies.keySet())
.forEach(lifetimeActionPolicies::remove);
Expand Down
Loading

0 comments on commit 0ca6e6d

Please sign in to comment.