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

Commit

Permalink
fix: select verification method from did document as per kid header v…
Browse files Browse the repository at this point in the history
…alue of JWT while verification
  • Loading branch information
nitin-vavdiya committed May 8, 2024
1 parent 7a3fbdd commit a3eb369
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 47 deletions.
100 changes: 53 additions & 47 deletions src/main/java/org/eclipse/tractusx/ssi/lib/jwt/SignedJwtVerifier.java
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,9 +75,14 @@ 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 {
throws DidParseException,
DidResolverException,
SignatureVerificationException,
SignatureParseException,
SignatureException,
SignatureVerificationFailedException {

JWTClaimsSet jwtClaimsSet;
try {
Expand All @@ -86,49 +97,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
Expand Up @@ -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
Expand Up @@ -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

0 comments on commit a3eb369

Please sign in to comment.