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 1ccb056578a..85425320e71 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 @@ -26,6 +26,8 @@ import org.jetbrains.annotations.Nullable; import java.text.ParseException; +import java.util.HashMap; +import java.util.function.Function; import java.util.regex.Pattern; import static org.eclipse.edc.iam.did.spi.document.DidConstants.ALLOWED_VERIFICATION_TYPES; @@ -58,10 +60,10 @@ protected Result resolveInternal(String id) { if (matcher.groupCount() > 1) { key = matcher.group(GROUP_FRAGMENT); } - return resolveDidPublicKey(did, key); + return resolveDidPublicKey(did, id, key); } - private Result resolveDidPublicKey(String didUrl, @Nullable String keyId) { + private Result resolveDidPublicKey(String didUrl, String verificationMethodUrl, @Nullable String keyId) { var didResult = resolverRegistry.resolve(didUrl); if (didResult.failed()) { return didResult.mapTo(); @@ -76,7 +78,7 @@ private Result resolveDidPublicKey(String didUrl, @Nullable String keyId .toList(); // if there are more than 1 verification methods with the same ID - if (verificationMethods.stream().map(VerificationMethod::getId).distinct().count() != verificationMethods.size()) { + if (verificationMethods.stream().map(verificationMethodIdMapper(didUrl)).distinct().count() != verificationMethods.size()) { return Result.failure("Every verification method must have a unique ID"); } Result verificationMethod; @@ -86,14 +88,14 @@ private Result resolveDidPublicKey(String didUrl, @Nullable String keyId 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 - verificationMethod = verificationMethods.stream().filter(vm -> vm.getId().equals(keyId)) + } else { // look up VerificationMethods by 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))); } return verificationMethod.compose(vm -> { - var key = vm.getPublicKeyJwk(); + var key = new HashMap<>(vm.getPublicKeyJwk()); key.put(JWKParameterNames.KEY_ID, vm.getId()); try { return Result.success(JWK.parse(key).toJSONString()); @@ -103,4 +105,15 @@ private Result resolveDidPublicKey(String didUrl, @Nullable String keyId }); } + + // If the verification method id is relative uri we map it to didUrl + id + private Function verificationMethodIdMapper(String didUrl) { + return (vm) -> { + if (vm.getId().startsWith(didUrl)) { + return vm.getId(); + } else { + return didUrl + vm.getId(); + } + }; + } } 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 631f11bd61d..6947649e814 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 @@ -48,7 +48,6 @@ class DidPublicKeyResolverImplTest { private final DidResolverRegistry resolverRegistry = mock(); private final KeyParserRegistry keyParserRegistry = mock(); private final DidPublicKeyResolverImpl resolver = new DidPublicKeyResolverImpl(keyParserRegistry, resolverRegistry, mock(), mock()); - private DidDocument didDocument; public static String readFile(String filename) throws IOException { try (var is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename)) { @@ -58,25 +57,58 @@ public static String readFile(String filename) throws IOException { } @BeforeEach - public void setUp() throws JOSEException, IOException { - var eckey = (ECKey) ECKey.parseFromPEMEncodedObjects(readFile("public_secp256k1.pem")); + public void setUp() throws JOSEException { - var vm = VerificationMethod.Builder.newInstance() - .id(KEYID) + when(keyParserRegistry.parse(anyString())).thenReturn(Result.success(new ECKeyGenerator(Curve.P_256).generate().toPublicKey())); + } + + private DidDocument createDidDocument(String verificationMethodId) { + return createDidDocumentBuilder(verificationMethodId).build(); + } + + private DidDocument createDidDocument() { + return createDidDocumentBuilder(KEYID).build(); + } + + private VerificationMethod createVerificationMethod(String verificationMethodId, ECKey eckey) { + return VerificationMethod.Builder.newInstance() + .id(verificationMethodId) .type(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019) .publicKeyJwk(eckey.toPublicJWK().toJSONObject()) .build(); + } - didDocument = DidDocument.Builder.newInstance() - .verificationMethod(List.of(vm)) - .service(Collections.singletonList(new Service("#my-service1", "MyService", "http://doesnotexi.st"))) - .build(); + private VerificationMethod createVerificationMethod(String verificationMethodId) { + try { + var eckey = (ECKey) ECKey.parseFromPEMEncodedObjects(readFile("public_secp256k1.pem")); + return createVerificationMethod(verificationMethodId, eckey); + } catch (JOSEException | IOException e) { + throw new RuntimeException(e); + } - when(keyParserRegistry.parse(anyString())).thenReturn(Result.success(new ECKeyGenerator(Curve.P_256).generate().toPublicKey())); + } + + private DidDocument.Builder createDidDocumentBuilder(String verificationMethodId) { + var vm = createVerificationMethod(verificationMethodId); + return DidDocument.Builder.newInstance() + .verificationMethod(List.of(vm)) + .service(Collections.singletonList(new Service("#my-service1", "MyService", "http://doesnotexi.st"))); } @Test void resolve() { + var didDocument = createDidDocument(); + when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); + + var result = resolver.resolveKey(DID_URL + KEYID); + + assertThat(result).isSucceeded().isNotNull(); + verify(resolverRegistry).resolve(DID_URL); + } + + @Test + void resolve_withVerificationMethodUrlAsId() throws IOException, JOSEException { + var didDocument = createDidDocument(DID_URL + KEYID); when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); var result = resolver.resolveKey(DID_URL + KEYID); @@ -97,6 +129,7 @@ void resolve_didNotFound() { @Test void resolve_didDoesNotContainPublicKey() { + var didDocument = createDidDocument(); didDocument.getVerificationMethod().clear(); when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); @@ -108,11 +141,22 @@ void resolve_didDoesNotContainPublicKey() { @Test void resolve_didContainsMultipleKeysWithSameKeyId() throws JOSEException, IOException { - var publicKey = (ECKey) ECKey.parseFromPEMEncodedObjects(readFile("public_secp256k1.pem")); - var vm = VerificationMethod.Builder.newInstance().id(KEYID).type(DidConstants.JSON_WEB_KEY_2020).controller("") - .publicKeyJwk(publicKey.toJSONObject()) - .build(); - didDocument.getVerificationMethod().add(vm); + var vm = createVerificationMethod(KEYID); + var didDocument = createDidDocumentBuilder(KEYID).verificationMethod(vm).build(); + when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); + + var result = resolver.resolveKey(DID_URL + KEYID); + + assertThat(result).isFailed() + .detail().contains("Every verification method must have a unique ID"); + verify(resolverRegistry).resolve(DID_URL); + } + + @Test + void resolve_didContainsMultipleKeysWithSameKeyId_withRelativeAndFullUrl() { + + var vm = createVerificationMethod(DID_URL + KEYID); + var didDocument = createDidDocumentBuilder(KEYID).verificationMethod(vm).build(); when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); var result = resolver.resolveKey(DID_URL + KEYID); @@ -124,15 +168,15 @@ void resolve_didContainsMultipleKeysWithSameKeyId() throws JOSEException, IOExce @Test void resolve_publicKeyNotInPemFormat() { - didDocument.getVerificationMethod().clear(); - var vm = VerificationMethod.Builder.newInstance().id("second-key").type(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019).controller("") + var secondKeyId = "#second-key"; + var vm = VerificationMethod.Builder.newInstance().id(secondKeyId).type(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019).controller("") .publicKeyJwk(Map.of("kty", "EC")) .build(); - didDocument.getVerificationMethod().add(vm); + var didDocument = createDidDocumentBuilder(KEYID).verificationMethod(vm).build(); when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); - var result = resolver.resolveKey(DID_URL + KEYID); + var result = resolver.resolveKey(DID_URL + secondKeyId); assertThat(result).isFailed(); verify(resolverRegistry).resolve(DID_URL); @@ -140,6 +184,7 @@ void resolve_publicKeyNotInPemFormat() { @Test void resolve_keyIdNullMultipleKeys() throws JOSEException, IOException { + var didDocument = createDidDocument(); var publicKey = (ECKey) ECKey.parseFromPEMEncodedObjects(readFile("public_secp256k1.pem")); var vm = VerificationMethod.Builder.newInstance().id("#my-key2").type(DidConstants.JSON_WEB_KEY_2020).controller("") .publicKeyJwk(publicKey.toJSONObject()) @@ -154,6 +199,7 @@ void resolve_keyIdNullMultipleKeys() throws JOSEException, IOException { @Test void resolve_keyIdIsNull_onlyOneVerificationMethod() { + var didDocument = createDidDocument(); when(resolverRegistry.resolve(DID_URL)).thenReturn(Result.success(didDocument)); var result = resolver.resolveKey(DID_URL); diff --git a/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidDocument.java b/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidDocument.java index 31120774540..ab51213a5ca 100644 --- a/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidDocument.java +++ b/spi/common/identity-did-spi/src/main/java/org/eclipse/edc/iam/did/spi/document/DidDocument.java @@ -95,6 +95,11 @@ public Builder verificationMethod(List verificationMethod) { return this; } + public Builder verificationMethod(VerificationMethod verificationMethod) { + document.verificationMethod.add(verificationMethod); + return this; + } + public Builder authentication(List authentication) { document.authentication.addAll(authentication); return this;