Skip to content

Commit

Permalink
feat(ldp): add LDP verifier, improve structure
Browse files Browse the repository at this point in the history
added LDP verifier, added SignatureSuiteRegistry to host multiple SignatureSuites, made the TestDocumentLoader more generally available,

BREAKING CHANGE: the TestResourceLoader was renamed to TestDocumentLoader, and moved
  • Loading branch information
paullatzelsperger committed Oct 24, 2023
1 parent cba9558 commit 7af34e4
Show file tree
Hide file tree
Showing 51 changed files with 3,796 additions and 365 deletions.
11 changes: 6 additions & 5 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
maven/mavencentral/com.apicatalog/carbon-did/0.0.2, Apache-2.0, approved, #9239

Check warning on line 1 in DEPENDENCIES

View workflow job for this annotation

GitHub Actions / check / Dash-Verify-Licenses

Restricted Dependencies found

Some dependencies are marked 'restricted' - please review them
maven/mavencentral/com.apicatalog/iron-ed25519-cryptosuite-2020/0.8.1, , restricted, clearlydefined
maven/mavencentral/com.apicatalog/iron-verifiable-credentials/0.8.1, Apache-2.0, approved, #9234
maven/mavencentral/com.apicatalog/titanium-json-ld/1.0.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.apicatalog/titanium-json-ld/1.3.1, Apache-2.0, approved, #8912
Expand Down Expand Up @@ -79,7 +80,7 @@ maven/mavencentral/com.jcraft/jzlib/1.1.3, BSD-2-Clause, approved, CQ6218
maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37, , restricted, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.37, Apache-2.0, approved, #11086
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.0, LGPL-2.1-or-later, approved, #7936
maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause, approved, clearlydefined
maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.11.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -123,10 +124,10 @@ maven/mavencentral/io.netty/netty-tcnative-boringssl-static/2.0.56.Final, Apache
maven/mavencentral/io.netty/netty-tcnative-classes/2.0.56.Final, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.netty/netty-transport-native-unix-common/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.netty/netty-transport/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.31.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.31.0, Apache-2.0, approved, #11085
maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.0.0-alpha, Apache-2.0, approved, #10044
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.31.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.31.0, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.31.0, Apache-2.0, approved, #11087
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.31.0, Apache-2.0, approved, #11088
maven/mavencentral/io.prometheus/simpleclient/0.16.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.prometheus/simpleclient_common/0.16.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.prometheus/simpleclient_httpserver/0.16.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -204,7 +205,7 @@ maven/mavencentral/org.apache.groovy/groovy/4.0.11, Apache-2.0 AND BSD-3-Clause
maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527
maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.13, Apache-2.0, approved, CQ23528
maven/mavencentral/org.apache.httpcomponents/httpmime/4.5.13, Apache-2.0, approved, CQ11718
maven/mavencentral/org.apache.kafka/kafka-clients/3.6.0, , restricted, clearlydefined
maven/mavencentral/org.apache.kafka/kafka-clients/3.6.0, Apache-2.0 AND (Apache-2.0 AND MIT) AND (Apache-2.0 AND BSD-3-Clause), approved, #11084
maven/mavencentral/org.apache.velocity.tools/velocity-tools-generic/3.1, Apache-2.0, approved, #9331
maven/mavencentral/org.apache.velocity/velocity-engine-core/2.3, Apache-2.0, approved, #2478
maven/mavencentral/org.apache.velocity/velocity-engine-scripting/2.3, Apache-2.0, approved, clearlydefined
Expand Down
8 changes: 7 additions & 1 deletion extensions/common/crypto/crypto-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ plugins {
}

dependencies {
api(project(":spi:common:identity-did-spi"))
api(project(":spi:common:identity-trust-spi"))
implementation(project(":core:common:util"))
implementation(project(":spi:common:core-spi"))
implementation(project(":extensions:common:iam:decentralized-identity:identity-did-crypto"))

implementation(libs.nimbus.jwt)
// used for the Ed25519 Verifier in conjunction with OctetKeyPairs (OKP)
runtimeOnly(libs.tink)

testImplementation(testFixtures(project(":core:common:junit")))
testImplementation(testFixtures(project(":spi:common:identity-trust-spi")))
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
*/

package org.eclipse.edc.iam.identitytrust.verification;
package org.eclipse.edc.verification.jwt;

import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.iam.did.crypto.JwtUtils;
Expand All @@ -39,9 +39,7 @@
* If no such {@code kid} header is present, then the <em>first</em> verification method is used.
* <p>
* Please note that <strong>no structural</strong> validation is done beyond the very basics (must have iss and aud claim).
* This is done by the {@link org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator}.
*
* @see org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator For SI Token validation.
* This is done by the {@link SelfIssuedIdTokenVerifier}.
*/
public class SelfIssuedIdTokenVerifier implements JwtVerifier {
private final DidResolverRegistry resolverRegistry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*
*/

package org.eclipse.edc.iam.identitytrust.verification;
package org.eclipse.edc.verification.jwt;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.Curve;
Expand Down
4 changes: 4 additions & 0 deletions extensions/common/crypto/jws2020/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
plugins {
`java-library`
`java-test-fixtures`
}

dependencies {
Expand All @@ -31,4 +32,7 @@ dependencies {
}

testImplementation(testFixtures(project(":core:common:junit")))
testFixturesImplementation(testFixtures(project(":core:common:junit")))
testFixturesImplementation(libs.nimbus.jwt)
testFixturesImplementation(project(":extensions:common:json-ld"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import java.io.ObjectOutputStream;
import java.net.URI;

record JwkMethod(URI id, URI type, URI controller, JWK keyPair) implements KeyPair {
public record JwkMethod(URI id, URI type, URI controller, JWK keyPair) implements KeyPair {

@Override
public byte[] privateKey() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public static LdSchema create(ObjectMapper mapper) {
.test(created -> Instant.now().isAfter(created))
.required(),
property(CONTROLLER, link()),
property(PURPOSE, link()).required(),
property(PURPOSE, link()).required().test(uri -> uri.toString().equals("https://w3id.org/security#assertionMethod")),
verificationMethod(VERIFICATION_METHOD, getVerificationMethod(mapper).map(new JwkAdapter())).required(),
property(DOMAIN, string())
.test((domain, params) -> !params.containsKey(DOMAIN.name()) || params.get(DOMAIN.name()).equals(domain)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class IssuerTests {

private final JwsSignature2020Suite jws2020suite = new JwsSignature2020Suite(JacksonJsonLd.createObjectMapper());
//used to load remote data from a local directory
private final TestResourcesLoader loader = new TestResourcesLoader("https://org.eclipse.edc/", "jws2020/issuing/", SchemeRouter.defaultInstance());
private final TestDocumentLoader loader = new TestDocumentLoader("https://org.eclipse.edc/", "jws2020/issuing/", SchemeRouter.defaultInstance());

@DisplayName("t0001: a simple credential to sign (EC Key)")
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class VerifierTests {

private final JwsSignature2020Suite jws2020suite = new JwsSignature2020Suite(JacksonJsonLd.createObjectMapper());
//used to load remote data from a local directory
private final TestResourcesLoader loader = new TestResourcesLoader("https://org.eclipse.edc/", "jws2020/verifying/", SchemeRouter.defaultInstance());
private final TestDocumentLoader loader = new TestDocumentLoader("https://org.eclipse.edc/", "jws2020/verifying/", SchemeRouter.defaultInstance());

@DisplayName("t0001: valid signed VC")
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
* For example, referencing a remote context, or a remote verificationMethod would fail, if that document doesn't exist, but we need it
* for testing, so we can "redirect" the pointer to the local test resources folder.
*/
class TestResourcesLoader implements DocumentLoader {
public class TestDocumentLoader implements DocumentLoader {
private final String base;
private final DocumentLoader baseLoader;
private final String resourcePath;

TestResourcesLoader(String base, String resourcePath, DocumentLoader baseLoader) {
public TestDocumentLoader(String base, String resourcePath, DocumentLoader baseLoader) {
this.base = base;
this.resourcePath = resourcePath;
this.baseLoader = baseLoader;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
package org.eclipse.edc.security.signature.jws2020;

import com.apicatalog.ld.signature.key.KeyPair;
import com.apicatalog.ld.signature.method.VerificationMethod;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import jakarta.json.JsonObject;
import org.eclipse.edc.jsonld.util.JacksonJsonLd;
Expand All @@ -26,11 +28,11 @@

import static org.eclipse.edc.junit.testfixtures.TestUtils.getResourceFileContentAsString;

class TestFunctions {
public class TestFunctions {

private static final ObjectMapper MAPPER = JacksonJsonLd.createObjectMapper();

static KeyPair createKeyPair(JWK jwk) {
public static KeyPair createKeyPair(JWK jwk) {
var id = URI.create("https://org.eclipse.edc/keys/" + UUID.randomUUID());
var type = URI.create("https://w3id.org/security#JsonWebKey2020");
return new JwkMethod(id, type, null, jwk);
Expand All @@ -43,4 +45,9 @@ static JsonObject readResourceAsJson(String name) {
throw new RuntimeException(e);
}
}

public static VerificationMethod createKeyPair(ECKey jwk, String id) {
var type = URI.create("https://w3id.org/security#JsonWebKey2020");
return new JwkMethod(URI.create(id), type, null, jwk);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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
*
*/
plugins {
`java-library`
`java-test-fixtures`
}

dependencies {
implementation(libs.nimbus.jwt)

implementation(project(":spi:common:json-ld-spi"))
implementation(project(":spi:common:identity-trust-spi"))
implementation(project(":core:common:util"))


testImplementation(project(":extensions:common:json-ld"))
testImplementation(project(":core:common:junit"))
testImplementation(project(":extensions:common:crypto:crypto-core"))
testFixturesImplementation(libs.nimbus.jwt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
*
*/

package org.eclipse.edc.iam.identitytrust.verification;
package org.eclipse.edc.verifiablecredentials.jwt;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jwt.SignedJWT;
import org.eclipse.edc.identitytrust.verification.CredentialVerifier;
import org.eclipse.edc.identitytrust.verification.JwtVerifier;
import org.eclipse.edc.identitytrust.verification.VerifierContext;
import org.eclipse.edc.spi.result.Result;

import java.text.ParseException;
Expand All @@ -26,7 +28,10 @@
import java.util.Map;

/**
* Computes the cryptographic integrity of a VerifiablePresentation when it's represented as JWT.
* Computes the cryptographic integrity of a VerifiablePresentation when it's represented as JWT. Internally, for the actual
* cryptographic computation it uses the generic {@link JwtVerifier} object. The main task of <em>this</em> class is to read the JWT,
* determine whether it's a VP or a VC and parse the contents.
* <p>
* In order to be successfully verified, a VP-JWT must contain a "vp" claim, that contains a JSON structure containing a
* "verifiableCredentials" object.
* This object contains an array of strings, each representing one VerifiableCredential, represented in JWT format.
Expand All @@ -43,82 +48,110 @@
* <li>iss: is used to resolve the DID, that contains the key-id</li>
* </ul>
* <li>A VP is only verified, if it and all VCs it contains are verified</li>
* </li>
* </ul>
*
* <em>Note: VP-JWTs may only contain VCs also represented in JWT format. Mixing formats is not allowed.</em>
*/
class JwtPresentationVerifier {
public class JwtPresentationVerifier implements CredentialVerifier {
public static final String VERIFIABLE_CREDENTIAL_JSON_KEY = "verifiableCredential";
public static final String VP_CLAIM = "vp";
public static final String VC_CLAIM = "vc";
private final JwtVerifier jwtVerifier;
private final String thisAudience;
private final ObjectMapper objectMapper;

/**
* Verifies the JWT presentation by checking the cryptographic integrity.
*
* @param jwtVerifier The JwtVerifier instance used to verify the JWT token.
* @param thisAudience The audience to which the token is intended. This must be "this connector's identifier"
* @param jwtVerifier The JwtVerifier instance used to verify the JWT token.
*/
JwtPresentationVerifier(JwtVerifier jwtVerifier, String thisAudience, ObjectMapper objectMapper) {
public JwtPresentationVerifier(JwtVerifier jwtVerifier, ObjectMapper objectMapper) {
this.jwtVerifier = jwtVerifier;
this.thisAudience = thisAudience;
this.objectMapper = objectMapper;
}


@Override
public boolean canHandle(String rawInput) {
try {
SignedJWT.parse(rawInput);
return true;
} catch (ParseException e) {
return false;
}
}

/**
* Verifies the presentation by checking the cryptographic integrity as well as the presence of mandatory claims in the JWT.
*
* @param serializedJwt The serialized JWT presentation to be verified.
* @return A Result object representing the verification result, containing specific error messages in case of failure.
*/
public Result<Void> verifyPresentation(String serializedJwt) {
@Override
public Result<Void> verify(String serializedJwt, VerifierContext context) {

// verify the "outer" JWT, i.e. the VP JWT
var vpResult = jwtVerifier.verify(serializedJwt, thisAudience);
if (vpResult.failed()) {
return vpResult;
var audience = context.getAudience();
var verificationResult = jwtVerifier.verify(serializedJwt, audience);
if (verificationResult.failed()) {
return verificationResult;
}

// verify all "inner" VC JWTs
try {
// obtain the actual VP JSON structure
var signedVpJwt = SignedJWT.parse(serializedJwt);
var vpClaim = signedVpJwt.getJWTClaimsSet().getClaim(VP_CLAIM);
if (vpClaim == null) {
return Result.failure("VP-JWT does not contain mandatory claim: " + VP_CLAIM);
// obtain the actual JSON structure
var signedJwt = SignedJWT.parse(serializedJwt);
if (isCredential(signedJwt)) {
return verificationResult;
}

if (!isPresentation(signedJwt)) {
return Result.failure("Either '%s' or '%s' claim must be present in JWT.".formatted(VP_CLAIM, VC_CLAIM));
}

var vpClaim = signedJwt.getJWTClaimsSet().getClaim(VP_CLAIM);
var vpJson = vpClaim.toString();

// obtain the "verifiableCredentials" object inside
var map = objectMapper.readValue(vpJson, Map.class);
if (!map.containsKey(VERIFIABLE_CREDENTIAL_JSON_KEY)) {
return Result.failure("Presentation object did not contain mandatory object: " + VERIFIABLE_CREDENTIAL_JSON_KEY);
}
var vcTokens = extractCredentials(map.get(VERIFIABLE_CREDENTIAL_JSON_KEY));
var rawCredentials = extractCredentials(map.get(VERIFIABLE_CREDENTIAL_JSON_KEY));

if (vcTokens.isEmpty()) {
if (rawCredentials.isEmpty()) {
// todo: this is allowed by the spec, but it is semantic nonsense. Should we return failure or not?
return Result.failure("No credential found in presentation.");
return Result.success();
}

// every VC is represented as another JWT, so we verify all of them
for (String token : vcTokens) {
vpResult = vpResult.merge(jwtVerifier.verify(token, signedVpJwt.getJWTClaimsSet().getIssuer()));
for (String token : rawCredentials) {
verificationResult = verificationResult.merge(context.withAudience(signedJwt.getJWTClaimsSet().getIssuer()).verify(token));
}

} catch (ParseException | JsonProcessingException e) {
throw new RuntimeException(e);
}
return vpResult;
return verificationResult;
}

private boolean isCredential(SignedJWT jwt) throws ParseException {
return jwt.getJWTClaimsSet().getClaims().containsKey(VC_CLAIM);
}

private boolean isPresentation(SignedJWT jwt) throws ParseException {
return jwt.getJWTClaimsSet().getClaims().containsKey(VP_CLAIM);
}

@SuppressWarnings("unchecked")
private List<String> extractCredentials(Object credentialsObject) {
if (credentialsObject instanceof Collection<?>) {
return ((Collection) credentialsObject).stream().map(Object::toString).toList();
return ((Collection) credentialsObject).stream().map(obj -> {
try {
return (obj instanceof String) ? obj.toString() : objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}).toList();
}
return List.of(credentialsObject.toString());
}
Expand Down
Loading

0 comments on commit 7af34e4

Please sign in to comment.