Skip to content

Commit

Permalink
add store-aware local key resolver service
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jun 3, 2024
1 parent 25e233f commit 4f3d373
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ private Result<String> generateOrGetKey(KeyDescriptor keyDescriptor) {
if (keyDescriptor.getKeyGeneratorParams() != null) {
var keyPair = KeyPairGenerator.generateKeyPair(keyDescriptor.getKeyGeneratorParams());
if (keyPair.failed()) {
return keyPair.mapTo();
return keyPair.mapFailure();
}
var privateJwk = CryptoConverter.createJwk(keyPair.getContent(), keyDescriptor.getKeyId());
publicKeySerialized = privateJwk.toPublicJWK().toJSONString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public Result<List<String>> verify(String token, String participantId) {
Objects.requireNonNull(participantId, "Participant ID is mandatory.");
var res = tokenValidationService.validate(token, publicKeyResolver, tokenValidationRulesRegistry.getRules(IATP_SELF_ISSUED_TOKEN_CONTEXT));
if (res.failed()) {
return res.mapTo();
return res.mapFailure();
}

var claimToken = res.getContent();
Expand Down Expand Up @@ -94,7 +94,7 @@ public Result<List<String>> verify(String token, String participantId) {
rules.add(audMustMatchParticipantIdRule);
var result = tokenValidationService.validate(accessTokenString, id -> Result.success(stsPublicKey.get()), rules);
if (result.failed()) {
return result.mapTo();
return result.mapFailure();
}

// verify that the access_token contains a scope claim
Expand Down
3 changes: 3 additions & 0 deletions core/lib/keypair-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ plugins {
}

dependencies {
implementation(project(":spi:identity-hub-store-spi"))
implementation(libs.edc.spi.core)
implementation(libs.edc.lib.util)
implementation(libs.edc.lib.keys)
testImplementation(libs.edc.junit)
testImplementation(libs.nimbus.jwt)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2024 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package org.eclipse.edc.identityhub.publickey;

import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.keys.LocalPublicKeyServiceImpl;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.query.Criterion;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.security.Vault;

import java.security.PublicKey;

/**
* This {@link org.eclipse.edc.keys.spi.LocalPublicKeyService} resolves this IdentityHub's own public keys by querying the {@link KeyPairResourceStore}.
* The rationale being that public keys should be represented as a {@link org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource}.
* <p>
* If no such {@link org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource} is found, this service will fall back to looking up the key in the vault. Note that this
* would be a strong indication of a data inconsistency.
*/
public class KeyPairResourcePublicKeyResolver extends LocalPublicKeyServiceImpl {

private final KeyPairResourceStore keyPairResourceStore;
private final KeyParserRegistry keyParserRegistry;
private final Monitor monitor;

public KeyPairResourcePublicKeyResolver(Vault vault, KeyPairResourceStore keyPairResourceStore, KeyParserRegistry registry, Monitor monitor) {
super(vault, registry);
this.keyPairResourceStore = keyPairResourceStore;
this.keyParserRegistry = registry;
this.monitor = monitor;
}

@Override
public Result<PublicKey> resolveKey(String id) {
return resolveFromDbOrVault(id);
}

private Result<PublicKey> resolveFromDbOrVault(String publicKeyId) {
var result = keyPairResourceStore.query(QuerySpec.Builder.newInstance().filter(new Criterion("keyId", "=", publicKeyId)).build());
// store failed, e.g. data model does not match query, etc.
if (result.failed()) {
monitor.warning("Error querying database for KeyPairResource with key ID '%s': %s".formatted(publicKeyId, result.getFailureDetail()));
return Result.failure(result.getFailureDetail());
}

var resources = result.getContent();
if (resources.size() > 1) {
monitor.warning("Expected exactly 1 KeyPairResource with keyId '%s' but found '%d'. This indicates a database inconsistency. Will return the first one.".formatted(publicKeyId, resources.size()));
}
return resources.stream().findAny()
.map(kpr -> parseKey(kpr.getSerializedPublicKey()))
.orElseGet(() -> {
monitor.warning("No KeyPairResource with keyId '%s' was found in the store. Will attempt to resolve from the Vault. This could be an indication of a data inconsistency, it is recommended to revoke and regenerate keys!");
return super.resolveKey(publicKeyId); // attempt to resolve from vault
});
}

// super-class's method is private, simply temporarily copy-pasted here
private Result<PublicKey> parseKey(String encodedKey) {
return keyParserRegistry.parse(encodedKey).compose(pk -> {
if (pk instanceof PublicKey publicKey) {
return Result.success(publicKey);
} else {
return Result.failure("The specified resource did not contain public key material.");
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (c) 2024 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package org.eclipse.edc.identityhub.publickey;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState;
import org.eclipse.edc.identityhub.spi.store.KeyPairResourceStore;
import org.eclipse.edc.keys.KeyParserRegistryImpl;
import org.eclipse.edc.keys.keyparsers.JwkParser;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.StoreResult;
import org.eclipse.edc.spi.security.Vault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.UUID;

import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

class KeyPairResourcePublicKeyResolverTest {

private final Vault vault = mock();
private final KeyPairResourceStore resourceStore = mock();
private final KeyParserRegistry parserRegistry = new KeyParserRegistryImpl();
private final Monitor monitor = mock();
private final KeyPairResourcePublicKeyResolver resolver = new KeyPairResourcePublicKeyResolver(vault, resourceStore, parserRegistry, monitor);

@BeforeEach
void setUp() {
parserRegistry.register(new JwkParser(new ObjectMapper(), monitor));
}

@Test
void resolveKey_whenFound() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(Collections.singletonList(createKeyPairResource().build())));

assertThat(resolver.resolveKey("test-key")).isSucceeded();
verify(resourceStore).query(any(QuerySpec.class));
verifyNoMoreInteractions(resourceStore);
verifyNoInteractions(vault, monitor);
}

@Test
void resolveKey_whenNotFoundInStore_foundInVault() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(Collections.emptyList()));
when(vault.resolveSecret(anyString())).thenReturn(createPublicKeyJwk());

assertThat(resolver.resolveKey("test-key")).isSucceeded();

verify(resourceStore).query(any(QuerySpec.class));
verify(vault).resolveSecret(anyString());
verify(monitor).warning(contains("Will attempt to resolve from the Vault."));
verifyNoMoreInteractions(vault, resourceStore);
}

@Test
void resolveKey_whenStoreFailure() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.notFound("foo-bar"));

assertThat(resolver.resolveKey("test-key")).isFailed().detail().isEqualTo("foo-bar");

verify(resourceStore).query(any(QuerySpec.class));
verify(monitor).warning(contains("Error querying database for KeyPairResource"));
verifyNoMoreInteractions(vault, resourceStore);
}

@Test
void resolveKey_whenNotFoundInResourceStore_notFoundInVault() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(Collections.emptyList()));
when(vault.resolveSecret(anyString())).thenReturn(null);

assertThat(resolver.resolveKey("test-key")).isFailed()
.detail()
.contains("No public key could be resolved for key-ID 'test-key'");

verify(resourceStore).query(any(QuerySpec.class));
verify(vault).resolveSecret(anyString());
verify(monitor).warning(contains("Will attempt to resolve from the Vault."));
verifyNoMoreInteractions(vault, resourceStore);
}

@Test
void resolveKey_whenMultipleFoundInStore() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(List.of(
createKeyPairResource().build(),
createKeyPairResource().build()
)));

assertThat(resolver.resolveKey("test-key")).isSucceeded();
verify(resourceStore).query(any(QuerySpec.class));
verify(monitor).warning(matches("Expected exactly 1 KeyPairResource with keyId '.*' but found '2'."));
verifyNoMoreInteractions(resourceStore, monitor);
}

@Test
void resolveKey_whenNotPublicKey() throws JOSEException {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(Collections.singletonList(createKeyPairResource()
.serializedPublicKey(new OctetKeyPairGenerator(Curve.Ed25519).generate().toJSONString()).build())));

assertThat(resolver.resolveKey("test-key")).isFailed()
.detail().contains("The specified resource did not contain public key material.");
verify(resourceStore).query(any(QuerySpec.class));
verifyNoMoreInteractions(resourceStore);
verifyNoInteractions(vault, monitor);
}

@Test
void resolveKey_whenInvalidFormat() {
when(resourceStore.query(any(QuerySpec.class))).thenReturn(StoreResult.success(Collections.singletonList(createKeyPairResource()
.serializedPublicKey("this-is-not-jwk-or-pem").build())));

assertThat(resolver.resolveKey("test-key")).isFailed()
.detail().contains("No parser found that can handle that format.");
verify(resourceStore).query(any(QuerySpec.class));
verifyNoMoreInteractions(resourceStore);
verifyNoInteractions(vault, monitor);
}

private KeyPairResource.Builder createKeyPairResource() {
return KeyPairResource.Builder.newInstance()
.id(UUID.randomUUID().toString())
.keyId(UUID.randomUUID().toString())
.isDefaultPair(true)
.state(KeyPairState.ACTIVE)
.serializedPublicKey(createPublicKeyJwk())
.privateKeyAlias("test-key-alias");
}

private String createPublicKeyJwk() {
try {
return new OctetKeyPairGenerator(Curve.Ed25519).generate().toPublicJWK().toJSONString();
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
}

0 comments on commit 4f3d373

Please sign in to comment.