Skip to content

Commit

Permalink
chore: align rotate keypair with dr (#454)
Browse files Browse the repository at this point in the history
* separate tests

* chore: align rotate-keypair operation with DR

* DEPENDENCIES
  • Loading branch information
paullatzelsperger authored Sep 12, 2024
1 parent 5884306 commit fccc410
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 51 deletions.
1 change: 1 addition & 0 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefine
maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, Apache-2.0, approved, #15156
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.41, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.18.1, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #16060
maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined
maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,14 +274,17 @@ private void keyPairActivated(KeyPairActivated event) {
var jwk = CryptoConverter.createJwk(new KeyPair((PublicKey) publicKey.getContent(), null));

var errors = didResources.stream()
.peek(dd -> dd.getDocument().getVerificationMethod().add(VerificationMethod.Builder.newInstance()
.id(event.getKeyId())
.publicKeyJwk(jwk.toJSONObject())
.controller(dd.getDocument().getId())
.type(event.getKeyType())
.build()))
.map(didResourceStore::update)
.filter(StoreResult::failed)
.map(dd -> {
dd.getDocument().getVerificationMethod().add(VerificationMethod.Builder.newInstance()
.id(event.getKeyId())
.publicKeyJwk(jwk.toJSONObject())
.controller(dd.getDocument().getId())
.type(event.getKeyType())
.build());
return ServiceResult.from(didResourceStore.update(dd))
.compose(v -> publish(dd.getDid()));
})
.filter(ServiceResult::failed)
.map(AbstractResult::getFailureDetail)
.collect(Collectors.joining(","));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,8 @@ void onKeyPairActivated() throws JOSEException {

when(didResourceStoreMock.query(any(QuerySpec.class))).thenReturn(List.of(didResource));
when(didResourceStoreMock.update(any())).thenReturn(StoreResult.success());
when(didResourceStoreMock.findById(eq(did))).thenReturn(didResource);
when(publisherMock.publish(did)).thenReturn(Result.success());

var event = EventEnvelope.Builder.newInstance()
.at(System.currentTimeMillis())
Expand All @@ -555,8 +557,9 @@ void onKeyPairActivated() throws JOSEException {

verify(didResourceStoreMock).query(any(QuerySpec.class));
verify(didResourceStoreMock).update(argThat(dr -> dr.getDocument().getVerificationMethod().stream().anyMatch(vm -> vm.getId().equals(keyId))));
verify(didResourceStoreMock).findById(did); // happens during the publishing
verifyNoMoreInteractions(didResourceStoreMock);
verifyNoInteractions(publisherMock);
verify(publisherMock).publish(eq(did));
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public ServiceResult<Void> addKeyPair(String participantId, KeyDescriptor keyDes
}

@Override
public ServiceResult<Void> rotateKeyPair(String oldId, @Nullable KeyDescriptor newKeySpec, long duration) {
public ServiceResult<Void> rotateKeyPair(String oldId, @Nullable KeyDescriptor newKeyDesc, long duration) {
return transactionContext.execute(() -> {
var oldKey = findById(oldId);
if (oldKey == null) {
Expand All @@ -142,8 +142,8 @@ public ServiceResult<Void> rotateKeyPair(String oldId, @Nullable KeyDescriptor n
var updateResult = ServiceResult.from(keyPairResourceStore.update(oldKey))
.onSuccess(v -> observable.invokeForEach(l -> l.rotated(oldKey)));

if (newKeySpec != null) {
return updateResult.compose(v -> addKeyPair(participantId, newKeySpec, wasDefault));
if (newKeyDesc != null) {
return updateResult.compose(v -> addKeyPair(participantId, newKeyDesc, wasDefault));
}
monitor.warning("Rotating keys without a successor key may leave the participant without an active keypair.");
return updateResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ void rotateKeyPair_oldKeyNotFound() {
verifyNoMoreInteractions(keyPairResourceStore, vault, observableMock);
}


@Test
void revokeKey_withNewKey() {
var oldId = "old-id";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import io.restassured.http.ContentType;
import io.restassured.http.Header;
import org.eclipse.edc.identithub.spi.did.events.DidDocumentPublished;
import org.eclipse.edc.identithub.spi.did.model.DidState;
import org.eclipse.edc.identithub.spi.did.store.DidResourceStore;
import org.eclipse.edc.identityhub.spi.keypair.events.KeyPairActivated;
Expand All @@ -33,6 +34,7 @@
import org.eclipse.edc.spi.event.EventRouter;
import org.eclipse.edc.spi.event.EventSubscriber;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.security.Vault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -49,6 +51,7 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
Expand Down Expand Up @@ -322,48 +325,84 @@ void addKeyPair_withoutActivate(IdentityHubEndToEndTestContext context, EventRou
}

@Test
void rotate(IdentityHubEndToEndTestContext context, EventRouter router) {
void rotate_withSuperUserToken(IdentityHubEndToEndTestContext context, EventRouter router) {
var superUserKey = context.createSuperUser();
var subscriber = mock(EventSubscriber.class);
router.registerSync(KeyPairRotated.class, subscriber);
router.registerSync(KeyPairAdded.class, subscriber);

var user1 = "user1";
var token = context.createParticipant(user1);
context.createParticipant(user1);

var keyPairId = context.createKeyPair(user1).getResourceId();

assertThat(Arrays.asList(token, superUserKey))
.allSatisfy(t -> {
reset(subscriber);
// attempt to publish user1's DID document, which should fail
var keyDesc = context.createKeyDescriptor(user1).build();
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", t))
.body(keyDesc)
.post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());
// attempt to publish user1's DID document, which should fail
var keyDesc = context.createKeyDescriptor(user1).build();
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", superUserKey))
.body(keyDesc)
.post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());

// verify that the "rotated" event fired once
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairRotated evt) {
return evt.getParticipantId().equals(user1);
}
return false;
}));
// verify that the correct "added" event fired
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairAdded evt) {
return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) &&
evt.getKeyId().equals(keyDesc.getKeyId());
}
return false;
}));
});
// verify that the "rotated" event fired once
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairRotated evt) {
return evt.getParticipantId().equals(user1);
}
return false;
}));
// verify that the correct "added" event fired
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairAdded evt) {
return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) &&
evt.getKeyId().equals(keyDesc.getKeyId());
}
return false;
}));
}

@Test
void rotate_withUserToken(IdentityHubEndToEndTestContext context, EventRouter router) {
var subscriber = mock(EventSubscriber.class);
router.registerSync(KeyPairRotated.class, subscriber);
router.registerSync(KeyPairAdded.class, subscriber);

var user1 = "user1";
var userToken = context.createParticipant(user1);

var keyPairId = context.createKeyPair(user1).getResourceId();

// attempt to publish user1's DID document, which should fail
var keyDesc = context.createKeyDescriptor(user1).build();
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", userToken))
.body(keyDesc)
.post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(user1), keyPairId))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());

// verify that the "rotated" event fired once
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairRotated evt) {
return evt.getParticipantId().equals(user1);
}
return false;
}));
// verify that the correct "added" event fired
verify(subscriber).on(argThat(env -> {
if (env.getPayload() instanceof KeyPairAdded evt) {
return evt.getKeyPairResourceId().equals(keyDesc.getResourceId()) &&
evt.getKeyId().equals(keyDesc.getKeyId());
}
return false;
}));
}

@Test
Expand Down Expand Up @@ -400,6 +439,86 @@ void rotate_notAuthorized(IdentityHubEndToEndTestContext context, EventRouter ro
}));
}

@Test
void rotate_withNewKey_shouldUpdateDidDocument(IdentityHubEndToEndTestContext context, EventRouter router, Vault vault) {
var subscriber = mock(EventSubscriber.class);
router.registerSync(KeyPairRotated.class, subscriber);
router.registerSync(KeyPairAdded.class, subscriber);

var participantId = "user1";
var userToken = context.createParticipant(participantId);
var keyPair = context.getKeyPairsForParticipant(participantId).stream().findFirst().orElseThrow();

var originalAlias = participantId + "-alias";
var originalKeyId = participantId + "-key";
var newPrivateKeyAlias = "new-alias";
var newKeyId = "new-keyId";
var keyDesc = context.createKeyDescriptor(participantId)
.active(true)
.privateKeyAlias(newPrivateKeyAlias)
.keyId(newKeyId)
.build();
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", userToken))
.body(keyDesc)
.post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(participantId), keyPair.getId()))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());

var didDoc = context.getDidForParticipant(participantId);
assertThat(didDoc).isNotEmpty()
.allSatisfy(doc -> assertThat(doc.getVerificationMethod()).hasSize(2)
.anyMatch(vm -> vm.getId().equals(originalKeyId)) // the original (now-rotated) key
.anyMatch(vm -> vm.getId().equals(newKeyId))); // the new key
assertThat(context.getKeyPairsForParticipant(participantId).stream().filter(kpr -> kpr.getKeyId().equals(originalKeyId)))
.allMatch(kpr -> kpr.getState() == KeyPairState.ROTATED.code());
assertThat(vault.resolveSecret(originalAlias)).isNull();
assertThat(vault.resolveSecret(newPrivateKeyAlias)).isNotNull();

}

@Test
void rotate_withNewKey_whenDidNotPublished_shouldNotUpdate(IdentityHubEndToEndTestContext context, EventRouter router) {
var subscriber = mock(EventSubscriber.class);
router.registerSync(KeyPairRotated.class, subscriber);
router.registerSync(KeyPairAdded.class, subscriber);
router.registerSync(DidDocumentPublished.class, subscriber);

var participantId = "user1";
var userToken = context.createParticipant(participantId, List.of(), false);
var keyPair = context.getKeyPairsForParticipant(participantId).stream().findFirst().orElseThrow();

var originalKeyId = participantId + "-key";
var newPrivateKeyAlias = "new-alias";
var newKeyId = "new-keyId";
var keyDesc = context.createKeyDescriptor(participantId)
.active(true)
.privateKeyAlias(newPrivateKeyAlias)
.keyId(newKeyId)
.build();
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", userToken))
.body(keyDesc)
.post("/v1alpha/participants/%s/keypairs/%s/rotate".formatted(toBase64(participantId), keyPair.getId()))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());

var didDoc = context.getDidForParticipant(participantId);
assertThat(didDoc).isNotEmpty()
.allSatisfy(doc -> assertThat(doc.getVerificationMethod()).hasSize(2)
.anyMatch(vm -> vm.getId().equals(originalKeyId)) // the original (now-rotated) key
.anyMatch(vm -> vm.getId().equals(newKeyId))); // the new key
assertThat(context.getKeyPairsForParticipant(participantId).stream().filter(kpr -> kpr.getKeyId().equals(originalKeyId)))
.allMatch(kpr -> kpr.getState() == KeyPairState.ROTATED.code());
verify(subscriber, never()).on(argThat(evt -> evt.getPayload() instanceof DidDocumentPublished));
}

@Test
void revoke(IdentityHubEndToEndTestContext context) {
var superUserKey = context.createSuperUser();
Expand Down Expand Up @@ -556,32 +675,36 @@ void activate_superUserToken(IdentityHubEndToEndTestContext context, EventRouter
void activate_userToken(IdentityHubEndToEndTestContext context, EventRouter router) {
var subscriber = mock(EventSubscriber.class);
router.registerSync(KeyPairActivated.class, subscriber);
router.registerSync(DidDocumentPublished.class, subscriber);

var user1 = "user1";
var token = context.createParticipant(user1);
assertThat(context.getDidForParticipant(user1))
var participantId = "user1";
var token = context.createParticipant(participantId);
assertThat(context.getDidForParticipant(participantId))
.allSatisfy(dd -> assertThat(dd.getVerificationMethod()).hasSize(1));

var keyDescriptor = context.createKeyPair(user1);
var keyDescriptor = context.createKeyDescriptor(participantId).active(false).build();
context.createKeyPair(participantId, keyDescriptor);
var keyPairId = keyDescriptor.getResourceId();

context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", token))
.post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(user1), keyPairId))
.post("/v1alpha/participants/%s/keypairs/%s/activate".formatted(toBase64(participantId), keyPairId))
.then()
.log().ifValidationFails()
.statusCode(204)
.body(notNullValue());

assertThat(context.getDidForParticipant(user1))
assertThat(context.getDidForParticipant(participantId))
.hasSize(1)
.allSatisfy(dd -> assertThat(dd.getVerificationMethod())
.hasSize(2)
.anyMatch(vm -> vm.getId().equals(keyDescriptor.getKeyId())));

assertThat(context.getDidResourceForParticipant("did:web:" + user1).getState()).isEqualTo(DidState.PUBLISHED.code());
assertThat(context.getDidResourceForParticipant("did:web:" + participantId).getState()).isEqualTo(DidState.PUBLISHED.code());
verify(subscriber).on(argThat(e -> e.getPayload() instanceof KeyPairActivated kpa && kpa.getKeyPairResourceId().equals(keyPairId)));
// publishes did when creating the user, and when activating
verify(subscriber, atLeast(2)).on(argThat(e -> e.getPayload() instanceof DidDocumentPublished));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ public Collection<KeyPairResource> getKeyPairsForParticipant(String participantI
public KeyDescriptor createKeyPair(String participantId) {

var descriptor = createKeyDescriptor(participantId).build();
return createKeyPair(participantId, descriptor);
}

public KeyDescriptor createKeyPair(String participantId, KeyDescriptor descriptor) {
var service = runtime.getService(KeyPairService.class);
service.addKeyPair(participantId, descriptor, true)
.orElseThrow(f -> new EdcException(f.getFailureDetail()));
Expand Down

0 comments on commit fccc410

Please sign in to comment.