Skip to content

Commit

Permalink
feat: disable consumer pull transfers if key settings are missing (#3820
Browse files Browse the repository at this point in the history
)

* feat: do not enable consumer pull transfer if settings are missing

* PR remarks
  • Loading branch information
ndr-brt authored Feb 1, 2024
1 parent c222b69 commit 695e83e
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@ public class JwtGenerationService implements TokenGenerationService {
public Result<TokenRepresentation> generate(Supplier<PrivateKey> privateKeySupplier, @NotNull TokenDecorator... decorators) {

var privateKey = privateKeySupplier.get();
if (privateKey == null) {
return Result.failure("PrivateKey cannot be resolved.");
}

var tokenSigner = CryptoConverter.createSignerFor(privateKey);
var jwsAlgorithm = CryptoConverter.getRecommendedAlgorithm(tokenSigner);


var bldr = TokenParameters.Builder.newInstance();
var allDecorators = new ArrayList<>(Arrays.asList(decorators));
allDecorators.add(new BaseDecorator(jwsAlgorithm));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
*/

package org.eclipse.edc.jwt;
package org.eclipse.edc.token;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSHeader;
Expand All @@ -23,7 +23,6 @@
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.token.JwtGenerationService;
import org.eclipse.edc.token.spi.TokenDecorator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -43,13 +42,6 @@ class JwtGenerationServiceTest {
private RSAKey keys;
private JwtGenerationService tokenGenerationService;

private static RSAKey testKey() throws JOSEException {
return new RSAKeyGenerator(2048)
.keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key
.keyID(UUID.randomUUID().toString()) // give the key a unique ID
.generate();
}

@BeforeEach
void setUp() throws JOSEException {
keys = testKey();
Expand Down Expand Up @@ -88,13 +80,29 @@ void verifyTokenGeneration() throws ParseException, JOSEException {
});
}

@Test
void shouldFail_whenPrivateKeyCannotBeResolved() {
var decorator = testDecorator();

var result = tokenGenerationService.generate(() -> null, decorator);

assertThat(result.failed()).isTrue();
}

private JWSVerifier createVerifier(JWSHeader header, Key publicKey) throws JOSEException {
return new DefaultJWSVerifierFactory().createJWSVerifier(header, publicKey);
}

private RSAKey testKey() throws JOSEException {
return new RSAKeyGenerator(2048)
.keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key
.keyID(UUID.randomUUID().toString()) // give the key a unique ID
.generate();
}

private TokenDecorator testDecorator() {
return (tokenParameters) -> tokenParameters.claims("foo", "bar")
.claims(EXPIRATION_TIME, Date.from(Instant.now().plusSeconds(60)))
.header("x5t", "some x509CertThumbprint thing");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@

public interface TransferDataPlaneConfig {


long DEFAULT_TOKEN_VALIDITY_SECONDS = 600; // 10min
@Setting(value = "Validity (in seconds) of tokens issued by the Control Plane for targeting the Data Plane public API. Default value: " + DEFAULT_TOKEN_VALIDITY_SECONDS, type = "long")
String TOKEN_VALIDITY_SECONDS = "edc.transfer.proxy.token.validity.seconds";

@Setting(value = "Alias of private key used for signing tokens, retrieved from private key resolver", defaultValue = "A random EC private key")
@Setting(value = "Alias of private key used for signing tokens, retrieved from private key resolver")
String TOKEN_SIGNER_PRIVATE_KEY_ALIAS = "edc.transfer.proxy.token.signer.privatekey.alias";

@Setting(value = "Alias of public key used for verifying the tokens, retrieved from the vault", defaultValue = "A random EC public key")
@Setting(value = "Alias of public key used for verifying the tokens, retrieved from the vault")
String TOKEN_VERIFIER_PUBLIC_KEY_ALIAS = "edc.transfer.proxy.token.verifier.publickey.alias";

String DEFAULT_DPF_SELECTOR_STRATEGY = "random";
@Setting(value = "Strategy for Data Plane instance selection", defaultValue = DEFAULT_DPF_SELECTOR_STRATEGY)
String DPF_SELECTOR_STRATEGY = "edc.transfer.client.selector.strategy";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.iam.LocalPublicKeyService;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
Expand All @@ -56,9 +55,6 @@ public class TransferDataPlaneCoreExtension implements ServiceExtension {
public static final String NAME = "Transfer Data Plane Core";
public static final String TRANSFER_DATAPLANE_TOKEN_CONTEXT = "dataplane-transfer";

@Inject
private Vault vault;

@Inject
private WebService webService;

Expand Down Expand Up @@ -111,27 +107,31 @@ public String name() {

@Override
public void initialize(ServiceExtensionContext context) {
var publicKeyAlias = context.getSetting(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, null);
var privateKeyAlias = context.getSetting(TOKEN_SIGNER_PRIVATE_KEY_ALIAS, null);

var pubKeyAlias = context.getSetting(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, null);
var privKeyAlias = context.getSetting(TOKEN_SIGNER_PRIVATE_KEY_ALIAS, null);
if (publicKeyAlias != null && privateKeyAlias != null) {
var controller = new ConsumerPullTransferTokenValidationApiController(tokenValidationService, dataEncrypter, typeManager, (i) -> publicKeyService.resolveKey(publicKeyAlias));
webService.registerResource(controlApiConfiguration.getContextAlias(), controller);

tokenValidationRulesRegistry.addRule(TRANSFER_DATAPLANE_TOKEN_CONTEXT, new ExpirationDateValidationRule(clock));
var resolver = new ConsumerPullDataPlaneProxyResolver(dataEncrypter, typeManager, new JwtGenerationService(), getPrivateKeySupplier(context, privateKeyAlias), () -> publicKeyAlias, tokenExpirationDateFunction);
dataFlowManager.register(new ConsumerPullTransferDataFlowController(selectorService, resolver));
} else {
context.getMonitor().info("One of these settings is not configured, so the connector won't be able to provide 'consumer-pull' transfers: [%s, %s]"
.formatted(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, TOKEN_SIGNER_PRIVATE_KEY_ALIAS));
}

var controller = new ConsumerPullTransferTokenValidationApiController(tokenValidationService, dataEncrypter, typeManager, (i) -> publicKeyService.resolveKey(pubKeyAlias));
webService.registerResource(controlApiConfiguration.getContextAlias(), controller);
tokenValidationRulesRegistry.addRule(TRANSFER_DATAPLANE_TOKEN_CONTEXT, new ExpirationDateValidationRule(clock));

var resolver = new ConsumerPullDataPlaneProxyResolver(dataEncrypter, typeManager, new JwtGenerationService(), getPrivateKeySupplier(context, privKeyAlias), () -> pubKeyAlias, tokenExpirationDateFunction);
dataFlowManager.register(new ConsumerPullTransferDataFlowController(selectorService, resolver));
dataFlowManager.register(new ProviderPushTransferDataFlowController(callbackUrl, selectorService, clientFactory));

dataAddressValidatorRegistry.registerDestinationValidator("HttpProxy", dataAddress -> ValidationResult.success());
}

@NotNull
private Supplier<PrivateKey> getPrivateKeySupplier(ServiceExtensionContext context, String privKeyAlias) {
return () -> privateKeyResolver.resolvePrivateKey(privKeyAlias)
private Supplier<PrivateKey> getPrivateKeySupplier(ServiceExtensionContext context, String privateKeyAlias) {
return () -> privateKeyResolver.resolvePrivateKey(privateKeyAlias)
.orElse(f -> {
context.getMonitor().warning(f.getFailureDetail());
context.getMonitor().warning("Cannot resolve private key: " + f.getFailureDetail());
return null;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,92 +14,60 @@

package org.eclipse.edc.connector.transfer.dataplane;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import org.eclipse.edc.connector.api.control.configuration.ControlApiConfiguration;
import org.eclipse.edc.connector.contract.spi.negotiation.store.ContractNegotiationStore;
import org.eclipse.edc.connector.dataplane.selector.spi.client.DataPlaneClient;
import org.eclipse.edc.connector.transfer.dataplane.api.ConsumerPullTransferTokenValidationApiController;
import org.eclipse.edc.connector.transfer.dataplane.flow.ConsumerPullTransferDataFlowController;
import org.eclipse.edc.connector.transfer.dataplane.flow.ProviderPushTransferDataFlowController;
import org.eclipse.edc.connector.transfer.dataplane.spi.security.DataEncrypter;
import org.eclipse.edc.connector.transfer.spi.flow.DataFlowManager;
import org.eclipse.edc.junit.extensions.DependencyInjectionExtension;
import org.eclipse.edc.spi.message.RemoteMessageDispatcherRegistry;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.security.PrivateKeyResolver;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.system.configuration.Config;
import org.eclipse.edc.spi.system.injection.ObjectFactory;
import org.eclipse.edc.web.spi.WebService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.io.IOException;
import java.security.KeyPair;
import java.util.Objects;
import java.util.UUID;

import static org.eclipse.edc.connector.transfer.dataplane.TransferDataPlaneConfig.TOKEN_SIGNER_PRIVATE_KEY_ALIAS;
import static org.eclipse.edc.connector.transfer.dataplane.TransferDataPlaneConfig.TOKEN_VERIFIER_PUBLIC_KEY_ALIAS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@ExtendWith(DependencyInjectionExtension.class)
class TransferDataPlaneCoreExtensionTest {

private static final String CONTROL_PLANE_API_CONTEXT = "control";

private final Vault vault = mock(Vault.class);
private final WebService webService = mock(WebService.class);
private final DataFlowManager dataFlowManager = mock(DataFlowManager.class);
private KeyPair keypair;
private TransferDataPlaneCoreExtension extension;

private static KeyPair keyPair() throws JOSEException {
return new RSAKeyGenerator(2048)
.keyUse(KeyUse.SIGNATURE) // indicate the intended use of the key
.keyID(UUID.randomUUID().toString()) // give the key a unique ID
.generate()
.toKeyPair();
}

private static String publicKeyPem() throws IOException {
return new String(Objects.requireNonNull(TransferDataPlaneCoreExtensionTest.class.getClassLoader().getResourceAsStream("rsa-pubkey.pem"))
.readAllBytes());
}
private final Vault vault = mock();
private final WebService webService = mock();
private final DataFlowManager dataFlowManager = mock();
private final Monitor monitor = mock();

@BeforeEach
public void setUp(ServiceExtensionContext context, ObjectFactory factory) throws JOSEException {
keypair = keyPair();
var monitor = mock(Monitor.class);
public void setUp(ServiceExtensionContext context) {
var controlApiConfigurationMock = mock(ControlApiConfiguration.class);
when(controlApiConfigurationMock.getContextAlias()).thenReturn(CONTROL_PLANE_API_CONTEXT);

context.registerService(PrivateKeyResolver.class, mock(PrivateKeyResolver.class));
context.registerService(Vault.class, mock(Vault.class));
context.registerService(WebService.class, webService);
context.registerService(ContractNegotiationStore.class, mock(ContractNegotiationStore.class));
context.registerService(RemoteMessageDispatcherRegistry.class, mock(RemoteMessageDispatcherRegistry.class));
context.registerService(DataFlowManager.class, dataFlowManager);
context.registerService(DataEncrypter.class, mock(DataEncrypter.class));
context.registerService(ControlApiConfiguration.class, controlApiConfigurationMock);
context.registerService(DataPlaneClient.class, mock(DataPlaneClient.class));
context.registerService(Vault.class, vault);

when(context.getMonitor()).thenReturn(monitor);

extension = factory.constructInstance(TransferDataPlaneCoreExtension.class);
}

@Test
void verifyInitializeSuccess(ServiceExtensionContext context) throws IOException {
void verifyInitializeSuccess(TransferDataPlaneCoreExtension extension, ServiceExtensionContext context) throws IOException {
var publicKeyAlias = "publicKey";
var privateKeyAlias = "privateKey";
var config = mock(Config.class);
Expand All @@ -114,4 +82,24 @@ void verifyInitializeSuccess(ServiceExtensionContext context) throws IOException
verify(dataFlowManager).register(any(ProviderPushTransferDataFlowController.class));
verify(webService).registerResource(eq(CONTROL_PLANE_API_CONTEXT), any(ConsumerPullTransferTokenValidationApiController.class));
}

@Test
void shouldNotRegisterConsumerPullControllers_whenSettingsAreMissing(TransferDataPlaneCoreExtension extension, ServiceExtensionContext context) {
var config = mock(Config.class);
when(context.getConfig()).thenReturn(config);
when(config.getString(TOKEN_VERIFIER_PUBLIC_KEY_ALIAS, null)).thenReturn(null);
when(config.getString(TOKEN_SIGNER_PRIVATE_KEY_ALIAS, null)).thenReturn(null);

extension.initialize(context);

verify(dataFlowManager, never()).register(isA(ConsumerPullTransferDataFlowController.class));
verifyNoInteractions(webService);
verify(monitor).info(any(String.class));
}

private String publicKeyPem() throws IOException {
try (var resource = TransferDataPlaneCoreExtensionTest.class.getClassLoader().getResourceAsStream("rsa-pubkey.pem")) {
return new String(Objects.requireNonNull(resource).readAllBytes());
}
}
}

0 comments on commit 695e83e

Please sign in to comment.