Skip to content

Commit

Permalink
refactor: minor improvements in Iatp and Did modules
Browse files Browse the repository at this point in the history
  • Loading branch information
bscholtes1A committed Feb 9, 2024
1 parent 8569352 commit 2ec0d52
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKParameterNames;
import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
Expand All @@ -26,22 +27,15 @@

import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Pattern;

import static java.lang.String.format;
import static java.util.Collections.emptyList;
import static org.eclipse.edc.iam.did.spi.document.DidConstants.ALLOWED_VERIFICATION_TYPES;

public class DidPublicKeyResolverImpl extends AbstractPublicKeyResolver implements DidPublicKeyResolver {
/**
* this regex pattern matches both DIDs and DIDs with a fragment (e.g. key-ID).
* Group 1 ("did") = the did:method:identifier portion
* Group 2 ("fragment") = the #fragment portion
*/
private static final Pattern PATTERN_DID_WITH_OPTIONAL_FRAGMENT = Pattern.compile("(?<did>did:.*:[^#]*)(?<fragment>#.*)?");
private static final String GROUP_DID = "did";
private static final String GROUP_FRAGMENT = "fragment";
private final DidResolverRegistry resolverRegistry;

public DidPublicKeyResolverImpl(KeyParserRegistry registry, DidResolverRegistry resolverRegistry) {
super(registry);
this.resolverRegistry = resolverRegistry;
Expand All @@ -67,32 +61,19 @@ private Result<String> resolveDidPublicKey(String didUrl, String verificationMet
if (didResult.failed()) {
return didResult.mapTo();
}

var didDocument = didResult.getContent();
if (didDocument.getVerificationMethod() == null || didDocument.getVerificationMethod().isEmpty()) {
return Result.failure("DID does not contain a public key");
var verificationMethods = validVerificationMethods(didDocument);
if (verificationMethods.isEmpty()) {
return Result.failure(format("DID document with id %s does not contain any supported Verification Method", didDocument.getId()));
}

var verificationMethods = didDocument.getVerificationMethod().stream()
.filter(vm -> ALLOWED_VERIFICATION_TYPES.contains(vm.getType()))
.toList();

// if there are more than 1 verification methods with the same ID
if (verificationMethods.stream().map(verificationMethodIdMapper(didUrl)).distinct().count() != verificationMethods.size()) {
return Result.failure("Every verification method must have a unique ID");
}
Result<VerificationMethod> verificationMethod;

if (keyId == null) { // only valid if exactly 1 verification method
if (verificationMethods.size() > 1) {
return Result.failure("The key ID ('kid') is mandatory if DID contains >1 verification methods.");
}
verificationMethod = Result.from(verificationMethods.stream().findFirst());
} else { // look up VerificationMethods by key ID or didUrl + key ID
verificationMethod = verificationMethods.stream().filter(vm -> vm.getId().equals(keyId) || vm.getId().equals(verificationMethodUrl))
.findFirst()
.map(Result::success)
.orElseGet(() -> Result.failure("No verification method found with key ID '%s'".formatted(keyId)));
}
var verificationMethod = selectVerificationMethod(verificationMethods, verificationMethodUrl, keyId);
return verificationMethod.compose(vm -> {
var key = new HashMap<>(vm.getPublicKeyJwk());
key.put(JWKParameterNames.KEY_ID, vm.getId());
Expand All @@ -105,6 +86,27 @@ private Result<String> resolveDidPublicKey(String didUrl, String verificationMet
});
}

private Result<VerificationMethod> selectVerificationMethod(List<VerificationMethod> verificationMethods, String verificationMethodUrl, @Nullable String keyId) {
if (keyId == null) { // only valid if exactly 1 verification method
return verificationMethods.size() == 1 ? Result.success(verificationMethods.get(0)) :
Result.failure("The key ID ('kid') is mandatory if DID contains >1 verification methods.");
}
// look up VerificationMethods by key ID or didUrl + key ID
return verificationMethods.stream().filter(vm -> vm.getId().equals(keyId) || vm.getId().equals(verificationMethodUrl))
.findFirst()
.map(Result::success)
.orElseGet(() -> Result.failure("No verification method found with key ID '%s'".formatted(keyId)));
}

private List<VerificationMethod> validVerificationMethods(DidDocument didDocument) {
if (didDocument.getVerificationMethod() == null) {
return emptyList();
}
return didDocument.getVerificationMethod().stream()
.filter(vm -> ALLOWED_VERIFICATION_TYPES.contains(vm.getType()))
.toList();
}

// If the verification method id is relative uri we map it to didUrl + id
private Function<VerificationMethod, String> verificationMethodIdMapper(String didUrl) {
return (vm) -> {
Expand All @@ -115,4 +117,14 @@ private Function<VerificationMethod, String> verificationMethodIdMapper(String d
}
};
}

/**
* this regex pattern matches both DIDs and DIDs with a fragment (e.g. key-ID).
* Group 1 ("did") = the did:method:identifier portion
* Group 2 ("fragment") = the #fragment portion
*/
private static final Pattern PATTERN_DID_WITH_OPTIONAL_FRAGMENT = Pattern.compile("(?<did>did:.*:[^#]*)(?<fragment>#.*)?");
private static final String GROUP_DID = "did";
private static final String GROUP_FRAGMENT = "fragment";
private final DidResolverRegistry resolverRegistry;
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ void resolve() {
}

@Test
void resolve_withVerificationMethodUrlAsId() throws IOException, JOSEException {
void resolve_withVerificationMethodUrlAsId() {
var didDocument = createDidDocument(DID_URL + KEYID);
when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument));

Expand Down Expand Up @@ -140,7 +140,7 @@ void resolve_didDoesNotContainPublicKey() {
}

@Test
void resolve_didContainsMultipleKeysWithSameKeyId() throws JOSEException, IOException {
void resolve_didContainsMultipleKeysWithSameKeyId() {
var vm = createVerificationMethod(KEYID);
var vm1 = createVerificationMethod(KEYID);
var didDocument = createDidDocumentBuilder(KEYID).verificationMethod(List.of(vm, vm1)).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,15 @@ public class IatpDefaultServicesExtension implements ServiceExtension {

@Setting(value = "Alias of private key used for signing tokens, retrieved from private key resolver", defaultValue = "A random EC private key")
public static final String STS_PRIVATE_KEY_ALIAS = "edc.iam.sts.privatekey.alias";
@Setting(value = "Alias of public key used for verifying the tokens, retrieved from the vault", defaultValue = "A random EC public key")
public static final String STS_PUBLIC_KEY_ALIAS = "edc.iam.sts.publickey.alias";
@Setting(value = "Id used by the counterparty to resolve the public key for token validation, e.g. did:example:123#public-key-0", defaultValue = "A random EC public key")
public static final String STS_PUBLIC_KEY_ID = "edc.iam.sts.publickey.id";
// not a setting, it's defined in Oauth2ServiceExtension
private static final String OAUTH_TOKENURL_PROPERTY = "edc.oauth.token.url";
@Setting(value = "Self-issued ID Token expiration in minutes. By default is 5 minutes", defaultValue = "" + IatpDefaultServicesExtension.DEFAULT_STS_TOKEN_EXPIRATION_MIN)
private static final String STS_TOKEN_EXPIRATION = "edc.iam.sts.token.expiration"; // in minutes
private static final int DEFAULT_STS_TOKEN_EXPIRATION_MIN = 5;


@Inject
private Clock clock;

@Inject
private PrivateKeyResolver privateKeyResolver;

Expand All @@ -71,7 +68,7 @@ public SecureTokenService createDefaultTokenService(ServiceExtensionContext cont
}


var publicKeyId = context.getSetting(STS_PUBLIC_KEY_ALIAS, null);
var publicKeyId = context.getSetting(STS_PUBLIC_KEY_ID, null);
var privKeyAlias = context.getSetting(STS_PRIVATE_KEY_ALIAS, null);

Supplier<PrivateKey> supplier = () -> privateKeyResolver.resolvePrivateKey(privKeyAlias).orElseThrow(f -> new EdcException("This EDC instance is not operational due to the following error: %s".formatted(f.getFailureDetail())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.iam.identitytrust.core.IatpDefaultServicesExtension.STS_PRIVATE_KEY_ALIAS;
import static org.eclipse.edc.iam.identitytrust.core.IatpDefaultServicesExtension.STS_PUBLIC_KEY_ALIAS;
import static org.eclipse.edc.iam.identitytrust.core.IatpDefaultServicesExtension.STS_PUBLIC_KEY_ID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
Expand Down Expand Up @@ -65,12 +65,12 @@ void setup(ServiceExtensionContext context) throws JOSEException {

@Test
void verify_defaultService(ServiceExtensionContext context, IatpDefaultServicesExtension ext) {
var publicAlias = "public";
var privateAlias = "private";
var publicKeyId = "did:web:" + UUID.randomUUID() + "#key-id";
var privateKeyAlias = "private";
Monitor mockedMonitor = mock();
context.registerService(Monitor.class, mockedMonitor);
when(context.getSetting(STS_PUBLIC_KEY_ALIAS, null)).thenReturn(publicAlias);
when(context.getSetting(STS_PRIVATE_KEY_ALIAS, null)).thenReturn(privateAlias);
when(context.getSetting(STS_PUBLIC_KEY_ID, null)).thenReturn(publicKeyId);
when(context.getSetting(STS_PRIVATE_KEY_ALIAS, null)).thenReturn(privateKeyAlias);
var sts = ext.createDefaultTokenService(context);

assertThat(sts).isInstanceOf(EmbeddedSecureTokenService.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,16 @@

package org.eclipse.edc.iam.did.spi.document;

import org.eclipse.edc.runtime.metamodel.annotation.Setting;

import java.util.Arrays;
import java.util.List;
import java.util.Set;

public interface DidConstants {
String ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019 = "EcdsaSecp256k1VerificationKey2019";
String JSON_WEB_KEY_2020 = "JsonWebKey2020";
String RSA_VERIFICATION_KEY_2018 = "RsaVerificationKey2018";
String ED_25519_VERIFICATION_KEY_2018 = "Ed25519VerificationKey2018";
List<String> ALLOWED_VERIFICATION_TYPES = Arrays.asList(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019,
Set<String> ALLOWED_VERIFICATION_TYPES = Set.of(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019,
DidConstants.JSON_WEB_KEY_2020,
DidConstants.RSA_VERIFICATION_KEY_2018,
DidConstants.ED_25519_VERIFICATION_KEY_2018);
@Setting
String DID_URL_SETTING = "edc.identity.did.url";

}

0 comments on commit 2ec0d52

Please sign in to comment.