diff --git a/src/main/java/org/eclipse/tractusx/ssi/lib/jwt/SignedJwtVerifier.java b/src/main/java/org/eclipse/tractusx/ssi/lib/jwt/SignedJwtVerifier.java index 8e13bc8e..20f6fad5 100644 --- a/src/main/java/org/eclipse/tractusx/ssi/lib/jwt/SignedJwtVerifier.java +++ b/src/main/java/org/eclipse/tractusx/ssi/lib/jwt/SignedJwtVerifier.java @@ -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; @@ -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 { @@ -86,49 +93,44 @@ public boolean verify(SignedJWT jwt) final DidDocument issuerDidDocument = didResolver.resolve(issuerDid); final List 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 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 toMap(List l) { + Map 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())); } } diff --git a/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/Ed25519VerificationMethod.java b/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/Ed25519VerificationMethod.java index ef67462c..562f6c87 100644 --- a/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/Ed25519VerificationMethod.java +++ b/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/Ed25519VerificationMethod.java @@ -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. @@ -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; @@ -77,4 +81,19 @@ public Ed25519VerificationMethod(Map 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(); + } } diff --git a/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/JWKVerificationMethod.java b/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/JWKVerificationMethod.java index 70b60741..79db30e4 100644 --- a/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/JWKVerificationMethod.java +++ b/src/main/java/org/eclipse/tractusx/ssi/lib/model/did/JWKVerificationMethod.java @@ -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. @@ -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; @@ -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"; @@ -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. * @@ -66,6 +72,45 @@ public JWKVerificationMethod(Map 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); } /** @@ -98,4 +143,14 @@ public static class PublicKeyJwk { private String crv; private String x; } + + private Map convertToMap(Object o) { + if (o instanceof Map) { + Map 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"); + } } diff --git a/src/test/java/org/eclipse/tractusx/ssi/lib/serialization/jwt/SerializedJwtPresentationFactoryImplTest.java b/src/test/java/org/eclipse/tractusx/ssi/lib/serialization/jwt/SerializedJwtPresentationFactoryImplTest.java index 7e39dc6d..bf1a4718 100644 --- a/src/test/java/org/eclipse/tractusx/ssi/lib/serialization/jwt/SerializedJwtPresentationFactoryImplTest.java +++ b/src/test/java/org/eclipse/tractusx/ssi/lib/serialization/jwt/SerializedJwtPresentationFactoryImplTest.java @@ -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; @@ -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. */ @@ -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 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 @@ -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() {