diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
new file mode 100644
index 0000000..887e8c4
--- /dev/null
+++ b/.github/workflows/check.yml
@@ -0,0 +1,50 @@
+name: Check PR
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - labeled
+ - unlabeled
+ branches:
+ - main
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '17'
+ java-package: jdk
+
+ - id: bump
+ uses: zwaldowski/match-label-action@v1
+ with:
+ allowed: major,minor,patch
+
+ - uses: zwaldowski/semver-release-action@v2
+ with:
+ dry_run: true
+ bump: ${{ steps.bump.outputs.match }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ comment:
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - uses: technote-space/workflow-conclusion-action@v2
+ - name: Checkout
+ uses: actions/checkout@v1
+
+ - name: Comment PR
+ if: env.WORKFLOW_CONCLUSION == 'failure'
+ uses: thollander/actions-comment-pull-request@1.0.2
+ with:
+ message: "Please apply one of the following labels to the PR: 'patch', 'minor', 'major'."
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..0b7bfc1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,97 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+
+ generate-version:
+ runs-on: ubuntu-latest
+
+ outputs:
+ version: ${{ steps.out.outputs.version }}
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '11'
+ java-package: jdk
+
+ - id: pr
+ uses: actions-ecosystem/action-get-merged-pull-request@v1.0.1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: zwaldowski/semver-release-action@v2
+ with:
+ dry_run: true
+ bump: patch
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set version output
+ id: out
+ run: echo "::set-output name=version::$(echo ${VERSION})"
+
+ build-and-deploy:
+
+ needs: [ "generate-version" ]
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - uses: actions/checkout@v2
+
+ - uses: crazy-max/ghaction-import-gpg@v5
+ with:
+ gpg_private_key: ${{ secrets.GPG_SECRET_KEY }}
+ passphrase: ${{ secrets.GPG_SECRET_KEY_PASSWORD }}
+ git_user_signingkey: true
+ git_commit_gpgsign: true
+
+ - uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 17
+ cache: maven
+ server-id: sonatype.org
+ server-username: SONATYPE_ORG_USERNAME
+ server-password: SONATYPE_ORG_PASSWORD
+ gpg-private-key: ${{ secrets.GPG_SECRET_KEY }}
+ gpg-passphrase: GPG_PASSPHRASE
+
+ - name: Set version
+ run: |
+ mvn versions:set -DnewVersion=${{ needs.generate-version.outputs.version }}
+
+ - name: Run tests
+ run: |
+ mvn clean test
+
+ - name: Build and release it
+ env:
+ SONATYPE_ORG_USERNAME: ${{ secrets.SONATYPE_ORG_USERNAME }}
+ SONATYPE_ORG_PASSWORD: ${{ secrets.SONATYPE_ORG_PASSWORD }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_SECRET_KEY_PASSWORD }}
+ run: |
+ mvn install deploy -Prelease -Dgpg.keyname=563C5DE0C079D6AD
+
+
+ git-release:
+ needs: [ "generate-version", "build-and-deploy" ]
+
+ runs-on: ubuntu-latest
+
+ steps:
+
+ - uses: actions/checkout@v2
+
+ - uses: "marvinpinto/action-automatic-releases@latest"
+ with:
+ repo_token: "${{ secrets.GITHUB_TOKEN }}"
+ automatic_release_tag: ${{ needs.generate-version.outputs.version }}
+ prerelease: false
+ title: ${{ needs.generate-version.outputs.version }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..66ed8a5
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,21 @@
+name: Test
+
+on:
+ push:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+
+ steps:
+
+ - uses: actions/checkout@v2
+
+ - uses: actions/setup-java@v1
+ with:
+ java-version: '21'
+ java-package: jdk
+
+ - name: Run tests
+ run: mvn clean test
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b865054
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# DCQL-Java
+
+A Java implementation of the [Digital Credentials Query Language(DCQL)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l).
+
+## Maven
+
+The library is avaliable at maven central:
+
+## Example usage
+
+In order to evaluate DCQL-Queries, a list of [VerifiableCredentials](https://en.wikipedia.org/wiki/Verifiable_credentials) has to be provided.
+The library itself uses a minimum of dependencies, therefor parsing of credentials and queries needs to be done by the caller.
+A possible option is [Jackson](https://github.com/FasterXML/jackson). In order to properly deserialize a query, the [ObjectMapper](https://www.baeldung.com/jackson-object-mapper-tutorial)
+needs to be configured as following:
+
+```java
+ ObjectMapper objectMapper = new ObjectMapper();
+ // future and backwards compatible, just ignore unsupported parts
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ // properties should be translated following snake-case, e.g. `claimSet` becomes `claim_set`and vice versa
+ objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+ SimpleModule deserializerModule = new SimpleModule();
+ // help deserialization of the enums. See test/java/io/github/wistefan/dcql/helper for their implementations
+ deserializerModule.addDeserializer(CredentialFormat.class, new CredentialFormatDeserializer());
+ deserializerModule.addDeserializer(TrustedAuthorityType.class, new TrustedAuthorityTypeDeserializer());
+ objectMapper.registerModule(deserializerModule);
+```
+
+Since credentials are usually not standard json-format, additional helper might be required. In case of sd-jwt and jwt credentials,
+a library like [Nimbus JOSE+JWT](https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt) can be used. See examples for loading SD and JWT credentials
+in the [ParseCredentialTest](./src/test/java/io/github/wistefan/dcql/example/ParseCredentialTest.java)
+
+After loading the credentials and providing query, evaluation is straight-forward:
+```java
+ // this configuration would support all CredentialFormats currently included in DCQL.
+ DCQLEvaluator dcqlEvaluator = new DCQLEvaluator(List.of(
+ new JwtCredentialEvaluator(),
+ new DcSdJwtCredentialEvaluator(),
+ new VcSdJwtCredentialEvaluator(),
+ new MDocCredentialEvaluator(),
+ new LdpCredentialEvaluator()));
+ QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(dcqlQuery, credentialsList);
+```
+
+The [QueryResult](./src/main/java/io/github/wistefan/dcql/QueryResult.java) provides a quick success indicator and the filtered list of credentials to be used.
+In case of SD-JWT Credentials, only the requested elements are disclosed.
+
+## Limitations
+
+As of now, DCQL-Java only supports querying for trusted authorities of type [Authority Key Identifier("aki")](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-authority-key-identifier).
+In order to do so, a [bouncycastle](https://www.bouncycastle.org/) implementation needs to be provided:
+
+```xml
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${version.org.bouncycastle}
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${version.org.bouncycastle}
+
+```
+
+## License
+
+DCQL-Java is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text.
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..9791971
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,299 @@
+
+
+ 4.0.0
+ io.github.wistefan
+ dcql-java
+ 0.0.1
+ jar
+
+
+
+ ${project.author.name}
+ ${project.author.email}
+
+
+
+ ${project.groupId}:${project.artifactId}
+ ${project.description}
+ ${project.url}
+
+
+ ${project.license.name}
+ ${project.license.url}
+
+
+
+ scm:git:git://github.com/wistefan/dcql-java.git
+ scm:git:git@github.com:wistefan/dcql-java.git
+ https://github.com/wistefan/dcql-java/tree/main
+ HEAD
+
+
+
+
+
+ sonatype.org
+ https://central.sonatype.com/repository/maven-snapshots/
+
+
+
+
+ 21
+ 21
+
+
+ Stefan Wiedemann
+ stefan.wiedemann@seamware.com
+ A Java-Implementation of the DCQL(Digital Credentials Query Language).
+
+ DCQL Java
+ https://github.com/wistefan/dcql-java
+ Apache License 2.0
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ DCQL Java
+ stefan.wiedemann@seamware.com
+ UTF-8
+ UTF-8
+
+
+ key
+ pass
+
+
+ 1.18.30
+
+ 2.0.17
+
+
+ 1.81
+
+
+ 3.6.1
+ 3.3.1
+
+ 3.14.0
+
+ 3.21.0
+ 4.9.4.2
+ 0.8.13
+ 3.11.3
+
+ 0.8.0
+ 3.2.8
+ 3.5.3
+
+
+
+ 5.13.4
+ 2.20.0
+ 10.5
+
+
+
+
+ org.projectlombok
+ lombok
+ ${version.org.projectlombok}
+ provided
+
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${version.org.bouncycastle}
+ provided
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${version.org.bouncycastle}
+ provided
+
+
+
+ org.slf4j
+ slf4j-api
+ ${version.org.slf4j}
+ provided
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${version.com.fasterxml.jackson.core}
+ test
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${version.com.fasterxml.jackson.core}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${version.org.junit.jupiter}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ ${version.org.junit.jupiter}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${version.org.junit.jupiter}
+ test
+
+
+ com.nimbusds
+ nimbus-jose-jwt
+ ${version.com.nimbusds.nimbus-jose-jwt}
+ test
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${version.org.codehaus.mojo.build-helper-maven-plugin}
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${version.org.apache.maven.plugins.maven-compiler-plugin}
+
+ ${jdk.version}
+ ${jdk.version}
+
+
+ org.projectlombok
+ lombok
+ ${version.org.projectlombok}
+
+
+
+
+
+ test-compile
+
+ testCompile
+
+
+
+
+ org.projectlombok
+ lombok
+ ${version.org.projectlombok}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ ${version.org.apache.maven.plugins.maven-site-plugin}
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${version.org.apache.maven.plugins.maven-surfire-plugin}
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${version.com.github.spotbugs.maven-plugin}
+
+ true
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${version.org.apache.maven.plugins.maven-javadoc-plugin}
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ ${version.org.apache.maven.plugins.maven-source-plugin}
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+
+
+
+
+
+ release
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ ${version.org.sonatype.central.publishing-plugin}
+ true
+
+ sonatype.org
+ true
+ published
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ ${version.org.apache.maven.plugins.maven-javadoc-plugin}
+
+ ${java.home}/bin/javadoc
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ ${version.org.apache.maven.plugins.maven-gpg-plugin}
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java
new file mode 100644
index 0000000..8d48bdd
--- /dev/null
+++ b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java
@@ -0,0 +1,237 @@
+package io.github.wistefan.dcql;
+
+import io.github.wistefan.dcql.model.ClaimsQuery;
+import io.github.wistefan.dcql.model.credential.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Evaluator for ClaimsQueries{@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-claims-query}
+ */
+@Slf4j
+public class ClaimsEvaluator {
+
+ // key for selective disclosure values inside the VC
+ private static final String SD_KEY = "_sd";
+
+ /**
+ * Evaluate claims query for MDoc-Credentials.
+ */
+ public static Optional evaluateClaimsForMDocCredential(ClaimsQuery claimsQuery, MDocCredential credential) {
+ List selectedClaims = new ArrayList<>();
+ try {
+ selectedClaims = selectClaimsByPath(credential.getPayload(), claimsQuery.getPath());
+ } catch (IllegalArgumentException iae) {
+ log.debug("Did not find the requested claims.", iae);
+ return Optional.empty();
+ }
+
+ if (claimsQuery.getValues() == null || claimsQuery.getValues().isEmpty()) {
+ return Optional.of(credential);
+ }
+
+ // checks if a value exists in the selected claims, that is not in the list of allowedValues.
+ if (selectedClaims.stream().allMatch(sC -> claimsQuery.getValues().contains(sC.value()))) {
+ return Optional.of(credential);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Evaluate claims query for SD-JWT-Credentials. The evaluator will check the disclosure and only include the requested
+ * disclosures in the resulting credential.
+ */
+ public static Optional evaluateClaimsForSdJwtCredential(ClaimsQuery claimsQuery, SdJwtCredential credential) {
+ List selectedClaims = new ArrayList<>();
+ try {
+ selectedClaims = selectClaimsByPathDisclosures(credential.getJwtCredential().getPayload(), claimsQuery.getPath(), credential.getDisclosures());
+ } catch (IllegalArgumentException iae) {
+ log.debug("Did not find the requested claims.", iae);
+ return Optional.empty();
+ }
+
+ if (claimsQuery.getValues() == null || claimsQuery.getValues().isEmpty()) {
+ return Optional.of(cleanUpDisclosures(selectedClaims, credential));
+ }
+
+ // checks if a value exists in the selected claims, that is not in the list of allowedValues.
+ if (selectedClaims.stream().allMatch(sC -> claimsQuery.getValues().contains(sC.value()))) {
+ return Optional.of(cleanUpDisclosures(selectedClaims, credential));
+ }
+ return Optional.empty();
+ }
+
+ private static SdJwtCredential cleanUpDisclosures(List selectedClaims, SdJwtCredential credential) {
+ Set hashsToInclude = selectedClaims.stream().map(SelectedClaim::hash).collect(Collectors.toSet());
+ List cleanedDisclosures = credential.getDisclosures()
+ .stream()
+ .filter(disclosure -> hashsToInclude.contains(disclosure.getSdHash()))
+ .toList();
+ return new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), cleanedDisclosures);
+ }
+
+ /**
+ * Evaluate the claims query for JWT Credentials
+ */
+ public static Optional evaluateClaimsForJwtCredential(ClaimsQuery claimsQuery, JwtCredential credential) {
+ List selectedClaims = new ArrayList<>();
+ try {
+ selectedClaims = selectClaimsByPath(credential.getPayload(), claimsQuery.getPath());
+ } catch (IllegalArgumentException iae) {
+ log.debug("Did not find the requested claims.", iae);
+ return Optional.empty();
+ }
+ if (claimsQuery.getValues() == null || claimsQuery.getValues().isEmpty()) {
+ return Optional.of(credential);
+ }
+
+ // checks if a value exists in the selected claims, that is not in the list of allowedValues.
+ if (selectedClaims.stream().allMatch(sC -> claimsQuery.getValues().contains(sC.value()))) {
+ return Optional.of(credential);
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Evaluate the claims query for LDP Credentials
+ */
+ public static Optional evaluateClaimsForLdpCredential(ClaimsQuery claimsQuery, LdpCredential credential) {
+ List selectedClaims = new ArrayList<>();
+ try {
+ selectedClaims = selectClaimsByPath(credential.getTheCredential(), claimsQuery.getPath());
+ } catch (IllegalArgumentException iae) {
+ log.debug("Did not find the requested claims.", iae);
+ return Optional.empty();
+ }
+ if (claimsQuery.getValues() == null || claimsQuery.getValues().isEmpty()) {
+ return Optional.of(credential);
+ }
+
+ // checks if a value exists in the selected claims, that is not in the list of allowedValues.
+ if (selectedClaims.stream().allMatch(sC -> claimsQuery.getValues().contains(sC.value()))) {
+ return Optional.of(credential);
+ }
+ return Optional.empty();
+ }
+
+
+ private static List selectClaimsByPath(Map credential, List