Skip to content
This repository has been archived by the owner on Jan 20, 2025. It is now read-only.

fix: select verification method from did document as per kid of JWT #99

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,31 @@
package org.eclipse.tractusx.ssi.lib.jwt;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.Ed25519Verifier;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory;
import com.nimbusds.jose.crypto.impl.ECDSAProvider;
import com.nimbusds.jose.crypto.impl.EdDSAProvider;
import com.nimbusds.jose.crypto.impl.RSASSAProvider;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jose.proc.JWSVerifierFactory;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import java.security.SignatureException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import lombok.SneakyThrows;
import org.eclipse.tractusx.ssi.lib.did.resolver.DidResolver;
import org.eclipse.tractusx.ssi.lib.exception.did.DidParseException;
import org.eclipse.tractusx.ssi.lib.exception.did.DidResolverException;
import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureParseException;
import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureVerificationException;
import org.eclipse.tractusx.ssi.lib.exception.proof.SignatureVerificationFailedException;
import org.eclipse.tractusx.ssi.lib.exception.proof.UnsupportedVerificationMethodException;
import org.eclipse.tractusx.ssi.lib.model.MultibaseString;
import org.eclipse.tractusx.ssi.lib.model.did.Did;
import org.eclipse.tractusx.ssi.lib.model.did.DidDocument;
import org.eclipse.tractusx.ssi.lib.model.did.DidParser;
Expand Down Expand Up @@ -69,6 +75,7 @@ public class SignedJwtVerifier {
* @throws SignatureException the signature exception
* @throws SignatureVerificationFailedException the signature verification failed exception
*/
@SneakyThrows
public boolean verify(SignedJWT jwt)
throws DidParseException, DidResolverException, SignatureVerificationException,
SignatureParseException, SignatureException, SignatureVerificationFailedException {
Expand All @@ -86,49 +93,44 @@ public boolean verify(SignedJWT jwt)
final DidDocument issuerDidDocument = didResolver.resolve(issuerDid);
final List<VerificationMethod> verificationMethods = issuerDidDocument.getVerificationMethods();

// verify JWT signature
// TODO Don't try out each key. Better -> use key authorization key
for (VerificationMethod verificationMethod : verificationMethods) {
if (JWKVerificationMethod.isInstance(verificationMethod)) {
final JWKVerificationMethod method = new JWKVerificationMethod(verificationMethod);
final String kty = method.getPublicKeyJwk().getKty();
final String crv = method.getPublicKeyJwk().getCrv();
final String x = method.getPublicKeyJwk().getX();
Map<String, VerificationMethod> verificationMethodMap = toMap(verificationMethods);

if (kty.equals("OKP") && crv.equals("Ed25519")) {
final OctetKeyPair keyPair =
new OctetKeyPair.Builder(Curve.Ed25519, Base64URL.from(x)).build();
try {
if (jwt.verify(new Ed25519Verifier(keyPair))) {
return true;
}
} catch (JOSEException e) {
throw new SignatureVerificationFailedException(e.getMessage());
}
} else {
throw new UnsupportedVerificationMethodException(
method, "only kty:OKP with crv:Ed25519 is supported");
}
} else if (Ed25519VerificationMethod.isInstance(verificationMethod)) {
final Ed25519VerificationMethod method = new Ed25519VerificationMethod(verificationMethod);
final MultibaseString multibase = method.getPublicKeyBase58();
final Ed25519PublicKeyParameters publicKeyParameters =
new Ed25519PublicKeyParameters(multibase.getDecoded(), 0);
final OctetKeyPair keyPair =
new OctetKeyPair.Builder(
Curve.Ed25519, Base64URL.encode(publicKeyParameters.getEncoded()))
.build();
String keyID = jwt.getHeader().getKeyID();
VerificationMethod verificationMethod = verificationMethodMap.get(keyID);
if (verificationMethod == null) {
throw new IllegalArgumentException(
String.format("no verification method for keyID %s found", keyID));
}

try {
if (jwt.verify(new Ed25519Verifier(keyPair))) {
return true;
}
} catch (JOSEException e) {
throw new SignatureVerificationFailedException(e.getMessage());
}
}
if (JWKVerificationMethod.isInstance(verificationMethod)) {
final JWKVerificationMethod method = new JWKVerificationMethod(verificationMethod);
JWSVerifier verifier = getVerifier(jwt.getHeader(), method.getJwk());
return jwt.verify(verifier);
} else if (Ed25519VerificationMethod.isInstance(verificationMethod)) {
final Ed25519VerificationMethod method = new Ed25519VerificationMethod(verificationMethod);
return jwt.verify(new Ed25519Verifier(method.getOctetKeyPair()));
} else {
return false;
}
}

return false;
private Map<String, VerificationMethod> toMap(List<VerificationMethod> l) {
Map<String, VerificationMethod> result = new HashMap<>();
l.forEach(v -> result.put(v.getId().toString(), v));
return result;
}

private JWSVerifier getVerifier(JWSHeader header, JWK key) throws JOSEException {
if (EdDSAProvider.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm())) {
return new Ed25519Verifier(((OctetKeyPair) key).toPublicJWK());
} else {
JWSVerifierFactory verifierFactory = new DefaultJWSVerifierFactory();
if (RSASSAProvider.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm()))
return verifierFactory.createJWSVerifier(header, key.toRSAKey().toRSAPublicKey());
if (ECDSAProvider.SUPPORTED_ALGORITHMS.contains(header.getAlgorithm()))
return verifierFactory.createJWSVerifier(header, key.toECKey().toPublicKey());
}
throw new IllegalArgumentException(
String.format("algorithm %s is not supported", header.getAlgorithm().getName()));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ******************************************************************************
* Copyright (c) 2021,2023 Contributors to the Eclipse Foundation
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand All @@ -21,9 +21,13 @@

package org.eclipse.tractusx.ssi.lib.model.did;

import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.OctetKeyPair;
import com.nimbusds.jose.util.Base64URL;
import java.util.Map;
import java.util.Objects;
import lombok.ToString;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.eclipse.tractusx.ssi.lib.model.MultibaseString;
import org.eclipse.tractusx.ssi.lib.model.base.MultibaseFactory;
import org.eclipse.tractusx.ssi.lib.serialization.SerializeUtil;
Expand Down Expand Up @@ -77,4 +81,19 @@ public Ed25519VerificationMethod(Map<String, Object> json) {
public MultibaseString getPublicKeyBase58() {
return MultibaseFactory.create((String) this.get(PUBLIC_KEY_BASE_58));
}

/**
* Gets public key base 58.
*
* @return the public key base 58
*/
public OctetKeyPair getOctetKeyPair() {
final MultibaseString multiBase = getPublicKeyBase58();
final Ed25519PublicKeyParameters publicKeyParameters =
new Ed25519PublicKeyParameters(multiBase.getDecoded(), 0);

return new OctetKeyPair.Builder(
Curve.Ed25519, Base64URL.encode(publicKeyParameters.getEncoded()))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* ******************************************************************************
* Copyright (c) 2021,2023 Contributors to the Eclipse Foundation
* Copyright (c) 2021,2024 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
Expand All @@ -21,6 +21,9 @@

package org.eclipse.tractusx.ssi.lib.model.did;

import com.nimbusds.jose.jwk.JWK;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import lombok.AllArgsConstructor;
Expand All @@ -31,6 +34,7 @@
/** The type Jwk verification method. */
@ToString
public class JWKVerificationMethod extends VerificationMethod {

/** The constant DEFAULT_TYPE. */
public static final String DEFAULT_TYPE = "JsonWebKey2020";

Expand All @@ -46,6 +50,8 @@ public class JWKVerificationMethod extends VerificationMethod {
/** The constant JWK_X. */
public static final String JWK_X = "x";

private final JWK jwk;

/**
* Instantiates a new Jwk verification method.
*
Expand All @@ -66,6 +72,45 @@ public JWKVerificationMethod(Map<String, Object> json) {
throw new IllegalArgumentException(
String.format("Invalid JsonWebKey2020: %s", SerializeUtil.toJson(json)), e);
}
Object object = this.get(PUBLIC_KEY_JWK);

try {
jwk = JWK.parse(convertToMap(object));
} catch (ParseException e) {
throw new IllegalStateException(e);
}
}

/**
* Gets jwk.
*
* @return the jwk
*/
public JWK getJwk() {
return jwk;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
if (!super.equals(o)) {
return false;
}
JWKVerificationMethod that = (JWKVerificationMethod) o;
return Objects.equals(jwk, that.jwk)
&& this.getId().equals(that.getId())
&& this.getType().equals(that.getType())
&& this.getController().equals(that.getController());
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), jwk);
}

/**
Expand Down Expand Up @@ -98,4 +143,14 @@ public static class PublicKeyJwk {
private String crv;
private String x;
}

private Map<String, Object> convertToMap(Object o) {
if (o instanceof Map) {
Map<String, Object> result = new HashMap<>();
Map<?, ?> rawMap = (Map<?, ?>) o;
rawMap.forEach((k, v) -> result.put((String) k, v));
return result;
}
throw new IllegalArgumentException("object is not a map");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@

package org.eclipse.tractusx.ssi.lib.serialization.jwt;

import static org.junit.Assert.assertNotNull;

import com.nimbusds.jwt.SignedJWT;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.UUID;
import lombok.SneakyThrows;
import org.eclipse.tractusx.ssi.lib.SsiLibrary;
import org.eclipse.tractusx.ssi.lib.crypt.octet.OctetKeyPairFactory;
Expand All @@ -43,6 +47,7 @@
import org.eclipse.tractusx.ssi.lib.util.vc.TestVerifiableFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/** The type Serialized jwt presentation factory impl test. */
Expand Down Expand Up @@ -79,6 +84,31 @@ public static void beforeAll() {
new Ed25519ProofSigner());
}

@Test
@DisplayName("Sign JWT with EdSA and verify signature")
void testJwtVerificationWithEdDSA() {
credentialIssuer = TestIdentityFactory.newIdentityWithED25519Keys();
didResolver.register(credentialIssuer);
jwtVerifier = new SignedJwtVerifier(didResolver);

LinkedHashMap<String, Object> claims = new LinkedHashMap<>();
SignedJwtFactory signedJwtFactory = new SignedJwtFactory(new OctetKeyPairFactory());
String keyId = "key-1";
// When

SignedJWT signedJWT =
signedJwtFactory.create(
credentialIssuer.getDid(),
credentialIssuer.getDid(),
claims,
credentialIssuer.getPrivateKey(),
keyId);

// Then
assertNotNull(signedJWT);
Assertions.assertDoesNotThrow(() -> jwtVerifier.verify(signedJWT));
}

/** Test jwt serialization. */
@SneakyThrows
@Test
Expand Down Expand Up @@ -110,6 +140,38 @@ void testJwtSerializationWithDefaultExpiration() {
/ 1000);
}

@Test
@DisplayName("Try to verify JWT when we do not have matching verification method in did document")
void testJwtSerializationWithInvalidKid() {
SerializedJwtPresentationFactory presentationFactory =
new SerializedJwtPresentationFactoryImpl(
new SignedJwtFactory(new OctetKeyPairFactory()),
new JsonLdSerializerImpl(),
credentialIssuer.getDid());

VerifiableCredential credentialWithProof = getCredential();

JwtConfig jwtConfig = JwtConfig.builder().expirationTime(CUSTOM_EXPIRATION_TIME).build();

// Build JWT
SignedJWT presentation =
presentationFactory.createPresentation(
credentialIssuer.getDid(),
List.of(credentialWithProof),
"test-audience",
credentialIssuer.getPrivateKey(),
"kid_"
+ UUID.randomUUID(), // pass random kid which will be not part of the did document
jwtConfig);

Assertions.assertNotNull(presentation);
try {
jwtVerifier.verify(presentation);
} catch (Exception e) {
Assertions.assertInstanceOf(IllegalArgumentException.class, e);
}
}

@SneakyThrows
@Test
void testJwtSerializationWithCustomExpiration() {
Expand Down
Loading