From 2ec0d525c42d0ba873922128fff4c5dee72db45d Mon Sep 17 00:00:00 2001 From: Benjamin SCHOLTES Date: Fri, 9 Feb 2024 11:48:15 +0100 Subject: [PATCH] refactor: minor improvements in Iatp and Did modules --- .../resolution/DidPublicKeyResolverImpl.java | 68 +++++++++++-------- .../DidPublicKeyResolverImplTest.java | 4 +- .../core/IatpDefaultServicesExtension.java | 9 +-- .../IatpDefaultServicesExtensionTest.java | 10 +-- .../iam/did/spi/document/DidConstants.java | 9 +-- 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/extensions/common/iam/decentralized-identity/identity-did-core/src/main/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImpl.java b/extensions/common/iam/decentralized-identity/identity-did-core/src/main/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImpl.java index 318d83b6122..ff1dcd7b432 100644 --- a/extensions/common/iam/decentralized-identity/identity-did-core/src/main/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImpl.java +++ b/extensions/common/iam/decentralized-identity/identity-did-core/src/main/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImpl.java @@ -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; @@ -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:.*:[^#]*)(?#.*)?"); - 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; @@ -67,32 +61,19 @@ private Result 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; - 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()); @@ -105,6 +86,27 @@ private Result resolveDidPublicKey(String didUrl, String verificationMet }); } + private Result selectVerificationMethod(List 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 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 verificationMethodIdMapper(String didUrl) { return (vm) -> { @@ -115,4 +117,14 @@ private Function 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:.*:[^#]*)(?#.*)?"); + private static final String GROUP_DID = "did"; + private static final String GROUP_FRAGMENT = "fragment"; + private final DidResolverRegistry resolverRegistry; } diff --git a/extensions/common/iam/decentralized-identity/identity-did-core/src/test/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImplTest.java b/extensions/common/iam/decentralized-identity/identity-did-core/src/test/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImplTest.java index 2224d806004..cefe700a1b6 100644 --- a/extensions/common/iam/decentralized-identity/identity-did-core/src/test/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImplTest.java +++ b/extensions/common/iam/decentralized-identity/identity-did-core/src/test/java/org/eclipse/edc/iam/did/resolution/DidPublicKeyResolverImplTest.java @@ -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)); @@ -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(); diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java index ebc919b4f95..273ce78dde2 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtension.java @@ -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; @@ -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 supplier = () -> privateKeyResolver.resolvePrivateKey(privKeyAlias).orElseThrow(f -> new EdcException("This EDC instance is not operational due to the following error: %s".formatted(f.getFailureDetail()))); diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java index f17a0a9c331..dd0762e75a9 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IatpDefaultServicesExtensionTest.java @@ -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; @@ -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); diff --git a/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidConstants.java b/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidConstants.java index 4be64c1f10f..5245b73157c 100644 --- a/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidConstants.java +++ b/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidConstants.java @@ -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 ALLOWED_VERIFICATION_TYPES = Arrays.asList(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019, + Set 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"; }