Skip to content

Commit

Permalink
fix: require key-id when resolving key material (#3655)
Browse files Browse the repository at this point in the history
feat: utilize the DidPublicKeyResolver in the JWT verification
  • Loading branch information
paullatzelsperger authored Nov 24, 2023
1 parent 21ac8b6 commit 1f76ae9
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.did.crypto.JwtUtils;
import org.eclipse.edc.iam.did.crypto.key.KeyConverter;
import org.eclipse.edc.iam.did.spi.document.DidConstants;
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.DidResolverRegistry;
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.spi.result.Result;
import org.jetbrains.annotations.NotNull;
Expand All @@ -42,10 +41,10 @@
* This is done by the {@link SelfIssuedIdTokenVerifier}.
*/
public class SelfIssuedIdTokenVerifier implements JwtVerifier {
private final DidResolverRegistry resolverRegistry;
private final DidPublicKeyResolver publicKeyResolver;

public SelfIssuedIdTokenVerifier(DidResolverRegistry resolverRegistry) {
this.resolverRegistry = resolverRegistry;
public SelfIssuedIdTokenVerifier(DidPublicKeyResolver publicKeyResolver) {
this.publicKeyResolver = publicKeyResolver;
}

@Override
Expand All @@ -54,26 +53,7 @@ public Result<Void> verify(String serializedJwt, String audience) {
SignedJWT jwt;
try {
jwt = SignedJWT.parse(serializedJwt);
var didResult = resolverRegistry.resolve(jwt.getJWTClaimsSet().getIssuer());
if (didResult.failed()) {
return Result.failure("Unable to resolve DID: %s".formatted(didResult.getFailureDetail()));
}

// this will return the _first_ public key entry
var keyId = jwt.getHeader().getKeyID();

//either get the first verification method, or the one specified by the key id
var publicKey = Optional.ofNullable(keyId)
.map(kid -> getVerificationMethod(didResult.getContent(), kid))
.orElseGet(() -> firstVerificationMethod(didResult.getContent()));

if (publicKey.isEmpty()) {
return Result.failure("Public Key not found in DID Document.");
}

//convert the POJO into a usable PK-wrapper:
var publicKeyJwk = publicKey.get().getPublicKeyJwk();
var publicKeyWrapperResult = KeyConverter.toPublicKeyWrapper(publicKeyJwk, publicKey.get().getId());
var publicKeyWrapperResult = publicKeyResolver.resolvePublicKey(jwt.getJWTClaimsSet().getIssuer(), jwt.getHeader().getKeyID());
if (publicKeyWrapperResult.failed()) {
return publicKeyWrapperResult.mapTo();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import com.nimbusds.jwt.JWTClaimsSet;
import org.eclipse.edc.iam.did.crypto.key.KeyConverter;
import org.eclipse.edc.iam.did.spi.document.DidConstants;
import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.Date;
import java.util.List;

import static org.eclipse.edc.identitytrust.TestFunctions.createJwt;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
Expand All @@ -40,8 +36,8 @@

class SelfIssuedIdTokenVerifierTest {

private final DidResolverRegistry didResolverRegistry = mock();
private final SelfIssuedIdTokenVerifier verifier = new SelfIssuedIdTokenVerifier(didResolverRegistry);
private final DidPublicKeyResolver pkResolver = mock();
private final SelfIssuedIdTokenVerifier verifier = new SelfIssuedIdTokenVerifier(pkResolver);
private ECKey didVerificationMethod;

@BeforeEach
Expand All @@ -56,10 +52,8 @@ void setUp() throws JOSEException {
.publicKeyJwk(didVerificationMethod.toPublicJWK().toJSONObject())
.build();

when(didResolverRegistry.resolve(any())).thenReturn(Result.success(DidDocument.Builder.newInstance()
.verificationMethod(List.of(vm))
.service(Collections.singletonList(new Service("#my-service1", "MyService", "http://doesnotexi.st")))
.build()));
var publicKeyWrapper = KeyConverter.toPublicKeyWrapper(didVerificationMethod.toPublicJWK().toJSONObject(), "#my-key1");
when(pkResolver.resolvePublicKey(any(), any())).thenReturn(publicKeyWrapper);
}

@Test
Expand All @@ -73,22 +67,6 @@ void verify_succeeds() {
assertThat(verifier.verify(createJwt(claimsSet, didVerificationMethod).getToken(), "test-audience")).isSucceeded();
}

@Test
void verify_didResolutionFailed() {
when(didResolverRegistry.resolve(any())).thenReturn(Result.failure("test failure"));
var jwt = createJwt();
assertThat(verifier.verify(jwt.getToken(), "test-audience")).isFailed()
.detail()
.isEqualTo("Unable to resolve DID: test failure");
}

@Test
void verify_publicKeyNotFound() {
assertThat(verifier.verify(createJwt().getToken(), "test-audience")).isFailed()
.detail()
.isEqualTo("Public Key not found in DID Document.");
}

@Test
void verify_verificationFailed_wrongSignature() throws JOSEException {
var signKey = new ECKeyGenerator(Curve.P_256)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ dependencies {
testImplementation(project(":core:common:junit"))
testImplementation(project(":extensions:common:crypto:crypto-core"))
testFixturesImplementation(libs.nimbus.jwt)
testFixturesImplementation(project(":spi:common:identity-did-spi"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,28 @@
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import org.eclipse.edc.iam.did.spi.document.DidConstants;
import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
import org.eclipse.edc.identitytrust.verification.VerifierContext;
import org.eclipse.edc.jsonld.util.JacksonJsonLd;
import org.eclipse.edc.junit.assertions.AbstractResultAssert;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.verification.jwt.SelfIssuedIdTokenVerifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.eclipse.edc.spi.result.Result.success;
import static org.eclipse.edc.verifiablecredentials.TestFunctions.createPublicKeyWrapper;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class JwtPresentationVerifierTest {

private final DidResolverRegistry didRegistryMock = mock();
private final DidPublicKeyResolver publicKeyResolverMock = mock();
private final ObjectMapper mapper = JacksonJsonLd.createObjectMapper();
private final JwtPresentationVerifier verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(publicKeyResolverMock), mapper);
private ECKey vpSigningKey;
private ECKey vcSigningKey;

Expand All @@ -58,28 +54,13 @@ void setup() throws JOSEException {
.keyID(TestConstants.CENTRAL_ISSUER_KEY_ID)
.generate();

var vpKeyWrapper = createPublicKeyWrapper(vpSigningKey);
when(publicKeyResolverMock.resolvePublicKey(eq(TestConstants.VP_HOLDER_ID), eq(TestConstants.PRESENTER_KEY_ID)))
.thenReturn(success(vpKeyWrapper));

// the DID document of the VP presenter (i.e. a participant agent)
var vpPresenterDid = DidDocument.Builder.newInstance()
.verificationMethod(List.of(VerificationMethod.Builder.create()
.id(TestConstants.PRESENTER_KEY_ID)
.type(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019)
.publicKeyJwk(vpSigningKey.toPublicJWK().toJSONObject())
.build()))
.service(Collections.singletonList(new Service("#my-service1", "MyService", "http://doesnotexi.st")))
.build();

// the DID document of the central issuer, e.g. a government body, etc.
var vcIssuerDid = DidDocument.Builder.newInstance()
.verificationMethod(List.of(VerificationMethod.Builder.create()
.id(TestConstants.CENTRAL_ISSUER_KEY_ID)
.type(DidConstants.ECDSA_SECP_256_K_1_VERIFICATION_KEY_2019)
.publicKeyJwk(vcSigningKey.toPublicJWK().toJSONObject())
.build()))
.build();

when(didRegistryMock.resolve(eq(TestConstants.VP_HOLDER_ID))).thenReturn(Result.success(vpPresenterDid));
when(didRegistryMock.resolve(eq(TestConstants.CENTRAL_ISSUER_DID))).thenReturn(Result.success(vcIssuerDid));
var vcKeyWrapper = createPublicKeyWrapper(vcSigningKey);
when(publicKeyResolverMock.resolvePublicKey(eq(TestConstants.CENTRAL_ISSUER_DID), eq(TestConstants.CENTRAL_ISSUER_KEY_ID)))
.thenReturn(success(vcKeyWrapper));
}

@Test
Expand All @@ -88,7 +69,7 @@ void verifyPresentation_noVpClaim() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "degreePres", TestConstants.MY_OWN_DID, Map.of());

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(publicKeyResolverMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -103,7 +84,6 @@ void verifyPresentation_noCredential() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "degreePres", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted("")));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -122,7 +102,6 @@ void verifyPresentation_invalidVpJson() {
}
"""));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -140,7 +119,6 @@ void verifyPresentation_singleVc_valid() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "degreePres", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted("\"" + vcJwt1 + "\"")));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -162,7 +140,6 @@ void verifyPresentation_multipleVc_valid() {
var vpContent = "\"%s\", \"%s\"".formatted(vcJwt1, vcJwt2);
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "testSub", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted(vpContent)));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), mapper);
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -184,7 +161,6 @@ void verifyPresentation_oneVcIsInvalid() throws JOSEException {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "degreePres", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted("\"" + vcJwt1 + "\"")));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -205,7 +181,6 @@ void verifyPresentation_vpJwtInvalid() throws JOSEException {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(spoofedKey, TestConstants.VP_HOLDER_ID, "degreePres", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted(vcJwt1)));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), mapper);
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -223,7 +198,6 @@ void verifyPresentation_vpJwt_invalidClaims() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, null, TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted(vcJwt1)));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -241,7 +215,6 @@ void verifyPresentation_vcJwt_invalidClaims() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "test-subject", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted("\"" + vcJwt1 + "\"")));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -259,7 +232,6 @@ void verifyPresentation_wrongAudience() {
// create VP-JWT (signed by the presenter) that contains the VP as a claim
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "test-pres-sub", "invalid-vp-audience", Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted(vcJwt1)));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand All @@ -281,7 +253,6 @@ void verifyPresentation_vcJwt_wrongAudience() {
var vpContent = "\"%s\", \"%s\"".formatted(vcJwt1, vcJwt2);
var vpJwt = JwtCreationUtils.createJwt(vpSigningKey, TestConstants.VP_HOLDER_ID, "testSub", TestConstants.MY_OWN_DID, Map.of("vp", TestConstants.VP_CONTENT_TEMPLATE.formatted(vpContent)));

var verifier = new JwtPresentationVerifier(new SelfIssuedIdTokenVerifier(didRegistryMock), new ObjectMapper());
var context = VerifierContext.Builder.newInstance()
.verifier(verifier)
.audience(TestConstants.MY_OWN_DID)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.verifiablecredentials;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEEncrypter;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import com.nimbusds.jose.crypto.ECDSAVerifier;
import com.nimbusds.jose.jwk.ECKey;
import org.eclipse.edc.iam.did.spi.key.PublicKeyWrapper;
import org.jetbrains.annotations.NotNull;

public class TestFunctions {

@NotNull
public static PublicKeyWrapper createPublicKeyWrapper(ECKey vpSigningKey) {
return new PublicKeyWrapper() {
@Override
public JWEEncrypter encrypter() {
try {
return new ECDHEncrypter(vpSigningKey);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}

@Override
public JWSVerifier verifier() {
try {
return new ECDSAVerifier(vpSigningKey);
} catch (JOSEException e) {
throw new RuntimeException(e);
}
}
};
}
}
Loading

0 comments on commit 1f76ae9

Please sign in to comment.