From c4f9479a18c736aeb1281bbbf018f67a3cdfc91f Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Thu, 11 Sep 2025 16:22:01 +0200 Subject: [PATCH 1/8] add lib implementation Signed-off-by: Stefan Wiedemann --- .github/workflows/check.yml | 50 +++ .github/workflows/release.yml | 98 ++++++ .github/workflows/test.yml | 21 ++ pom.xml | 291 +++++++++++++++++ .../github/wistefan/dcql/ClaimsEvaluator.java | 220 +++++++++++++ .../wistefan/dcql/CredentialMapper.java | 68 ++++ .../github/wistefan/dcql/DCQLEvaluator.java | 302 ++++++++++++++++++ .../dcql/TrustedAuthoritiesEvaluator.java | 89 ++++++ .../wistefan/dcql/model/ClaimsQuery.java | 60 ++++ .../wistefan/dcql/model/Credential.java | 14 + .../wistefan/dcql/model/CredentialFormat.java | 37 +++ .../wistefan/dcql/model/CredentialQuery.java | 69 ++++ .../dcql/model/CredentialSetQuery.java | 27 ++ .../github/wistefan/dcql/model/DcqlQuery.java | 27 ++ .../wistefan/dcql/model/JwtMetaData.java | 30 ++ .../wistefan/dcql/model/MDocMetaData.java | 24 ++ .../dcql/model/TrustedAuthorityQuery.java | 28 ++ .../dcql/model/TrustedAuthorityType.java | 34 ++ .../wistefan/dcql/model/W3CMetaData.java | 45 +++ .../dcql/model/credential/Disclosure.java | 16 + .../dcql/model/credential/JwtCredential.java | 63 ++++ .../dcql/model/credential/LdpCredential.java | 32 ++ .../dcql/model/credential/MDocCredential.java | 25 ++ .../dcql/model/credential/MDocHeaders.java | 17 + .../model/credential/SdJwtCredential.java | 24 ++ .../dcql/result/ClaimEvaluationResult.java | 17 + .../wistefan/dcql/result/ClaimSetResult.java | 18 ++ .../wistefan/dcql/result/ClaimsResult.java | 19 ++ .../result/CredentialEvaluationResult.java | 14 + .../wistefan/dcql/result/CredentialMatch.java | 14 + .../dcql/result/CredentialSetResult.java | 14 + .../wistefan/dcql/result/MetaResult.java | 14 + .../wistefan/dcql/result/QueryResult.java | 17 + .../dcql/result/TrustedAuthoritiesResult.java | 15 + .../TrustedAuthorityEvaluationResult.java | 16 + .../wistefan/dcql/ClaimsEvaluatorTest.java | 77 +++++ .../dcql/query/DcqlClaimSetQueryTest.java | 245 ++++++++++++++ .../dcql/query/DcqlQueryComplexTest.java | 172 ++++++++++ .../wistefan/dcql/query/DcqlQueryTest.java | 262 +++++++++++++++ .../DcqlQueryTrustedAuthoritiesTest.java | 139 ++++++++ .../query/DcqlQueryWithJsonTransformTest.java | 127 ++++++++ .../github/wistefan/dcql/query/DcqlTest.java | 105 ++++++ 42 files changed, 2996 insertions(+) create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 pom.xml create mode 100644 src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/CredentialMapper.java create mode 100644 src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/TrustedAuthoritiesEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/Credential.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/MetaResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/QueryResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java create mode 100644 src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java create mode 100644 src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java create mode 100644 src/test/java/io/github/wistefan/dcql/query/DcqlTest.java 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..e60bac9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,98 @@ +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 jacoco:report + #coveralls:report -Dcoveralls.token=${{ secrets.COVERALLS_TOKEN }} + + - 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/pom.xml b/pom.xml new file mode 100644 index 0000000..992324b --- /dev/null +++ b/pom.xml @@ -0,0 +1,291 @@ + + + 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.20.0 + 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 + + + + + com.fasterxml.jackson.core + jackson-databind + ${version.com.fasterxml.jackson.core} + provided + + + com.fasterxml.jackson.core + jackson-core + ${version.com.fasterxml.jackson.core} + + + 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 + + + + 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 + + + + + + + + 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..06787f0 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java @@ -0,0 +1,220 @@ +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.util.*; +import java.util.stream.Collectors; + +@Slf4j +public class ClaimsEvaluator { + + private static final String SD_KEY = "_sd"; + + 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(); + } + + 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.getHash())) + .toList(); + return new SdJwtCredential(credential.getJwtCredential(), cleanedDisclosures); + } + + 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(); + } + + public static Optional evaluateClaimsForLdpCredential(ClaimsQuery claimsQuery, LdpCredential credential) { + List selectedClaims = new ArrayList<>(); + try { + selectedClaims = selectClaimsByPath(credential, 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(); + } + + + public static List selectClaimsByPath(Map credential, List claimPath) { + return processPath(credential, claimPath, null); + } + + public static List selectClaimsByPathDisclosures(Map credential, List claimPath, + List disclosures) { + return processPath(credential, claimPath, disclosures); + } + + private static List processPath( + Map credential, + List claimPath, + List disclosures + ) { + if (credential == null || claimPath == null || claimPath.isEmpty()) { + throw new IllegalArgumentException("Credential and claimPath must not be null or empty"); + } + + // Start with root + List current = new ArrayList<>(); + current.add(new SelectedClaim(credential, null)); + + for (Object component : claimPath) { + List nextSelection = new ArrayList<>(); + + for (SelectedClaim candidateWrapper : current) { + Object candidate = candidateWrapper.value; + + // If map contains _sd, reveal it and MERGE revealed entries with the original map + if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey("_sd")) { + Object sdObj = mapCandidate.get("_sd"); + Map revealed = getStringSelectedClaimMap(disclosures, sdObj); + + // Merge: start with revealed, then copy original entries (except "_sd"), + // so explicit values in the original map overwrite revealed ones if keys collide. + Map merged = new LinkedHashMap<>(); + merged.putAll(revealed); + for (Map.Entry e : mapCandidate.entrySet()) { + String k = String.valueOf(e.getKey()); + if (SD_KEY.equals(k)) continue; + merged.put(k, e.getValue()); + } + candidate = merged; + } + + // Process path component + if (component instanceof String key) { + if (!(candidate instanceof Map map)) { + throw new IllegalArgumentException("Expected object for key lookup but found: " + candidate); + } + if (map.containsKey(key)) { + Object val = map.get(key); + if (val instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(val, null)); + } + } + } else if (component == null) { + if (!(candidate instanceof List list)) { + throw new IllegalArgumentException("Expected array for null selector but found: " + candidate); + } + for (Object elem : list) { + if (elem instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(elem, null)); + } + } + } else if (component instanceof Integer index && index >= 0) { + if (!(candidate instanceof List list)) { + throw new IllegalArgumentException("Expected array for index selector but found: " + candidate); + } + if (index < list.size()) { + Object val = list.get(index); + if (val instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(val, null)); + } + } + } else { + throw new IllegalArgumentException("Invalid claim path component: " + component); + } + } + + if (nextSelection.isEmpty()) { + throw new IllegalArgumentException("No elements selected at path component: " + component); + } + + current = nextSelection; + } + + return current; + } + + + private static Map getStringSelectedClaimMap(List disclosures, Object sdObj) { + if (!(sdObj instanceof List sdList)) { + throw new IllegalArgumentException("_sd field must be a list"); + } + + Map revealed = new LinkedHashMap<>(); + for (Object hashObj : sdList) { + if (!(hashObj instanceof String hash)) continue; + for (Disclosure disclosure : disclosures) { + if (hash.equals(disclosure.getHash())) { + revealed.put(disclosure.getClaim(), new SelectedClaim(disclosure.getValue(), disclosure.getHash())); + } + } + } + return revealed; + } + + private record SelectedClaim(Object value, String hash) { + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/CredentialMapper.java b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java new file mode 100644 index 0000000..657f5bf --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java @@ -0,0 +1,68 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.LdpCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; + +import java.util.ArrayList; +import java.util.List; + +public class CredentialMapper { + + public static List toCredentials(CredentialFormat credentialFormat, List rawCredentials) { + return rawCredentials.stream() + .map(rC -> new Credential(credentialFormat, rC)) + .toList(); + } + + public static List toLdpCredentials(List credentialsList) { + List ldpCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof LdpCredential ldpCredential) { + ldpCredentialsList.add(ldpCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an ldp_vc."); + } + } + return ldpCredentialsList; + } + + public static List toMDocCredentials(List credentialsList) { + List mDocCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof MDocCredential mDocCredential) { + mDocCredentialsList.add(mDocCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an mso_mdoc."); + } + } + return mDocCredentialsList; + } + + public static List toJWTCredentials(List credentialsList) { + List jwtCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof JwtCredential jwtCredential) { + jwtCredentialsList.add(jwtCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an jwt_vc_json."); + } + } + return jwtCredentialsList; + } + + public static List toSdJWTCredentials(List credentialsList) { + List sdJwtCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof SdJwtCredential sdJWTCredential) { + sdJwtCredentialsList.add(sdJWTCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an vc+sd-jwt/dc+sd-jwt."); + } + } + return sdJwtCredentialsList; + } +} diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java new file mode 100644 index 0000000..cfe0909 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -0,0 +1,302 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.*; +import io.github.wistefan.dcql.model.credential.*; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +@Slf4j +public class DCQLEvaluator { + + private static final String MDOC_NAMESPACE_KEY = "namespaces"; + + public static List evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { + List selectedCredentials = new ArrayList<>(); + for (CredentialQuery cq : dcqlQuery.getCredentials()) { + List credentialsFullfilling = evaluateCredentialQuery(cq, credentialsList); + if (credentialsFullfilling.isEmpty()) { + log.debug("When one of the credentials requirements is not fulfilled, the query should fail."); + return List.of(); + } + selectedCredentials.addAll(credentialsFullfilling); + } + return selectedCredentials; + } + + private static List evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { + + if (!containsClaims(credentialQuery) + && containsClaims(credentialQuery)) { + throw new IllegalArgumentException("Queries with claim_set require to have claims, too."); + } + + List filteredByFormat = filterByFormat(credentialQuery.getFormat(), credentialsList); + return switch (credentialQuery.getFormat()) { + case LDP_VC -> evaluateForLdpVC(credentialQuery, filteredByFormat); + case MSO_MDOC -> evaluateForMDoc(credentialQuery, filteredByFormat); + case DC_SD_JWT, VC_SD_JWT -> evaluateForSdJwt(credentialQuery, filteredByFormat); + case JWT_VC_JSON -> evaluateForJwt(credentialQuery, filteredByFormat); + }; + } + + private static List evaluateForSdJwt(CredentialQuery credentialQuery, List credentialsList) { + List sdJwtCredentials = CredentialMapper.toSdJWTCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + sdJwtCredentials = filterSdJwtByMetadata(credentialQuery.getMeta(), sdJwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + sdJwtCredentials = sdJwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForSDJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + sdJwtCredentials = evaluateSdJwtCredentialsQuery(credentialQuery, sdJwtCredentials); + } else if (containsClaims(credentialQuery)) { + return evaluateSdJwtForClaimSet(credentialQuery, sdJwtCredentials); + } else { + sdJwtCredentials = sdJwtCredentials.stream() + // keep the original credential untouched + .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getJwtCredential(), List.of())) + .toList(); + } + return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); + } + + private static List evaluateForJwt(CredentialQuery credentialQuery, List credentialsList) { + List jwtCredentials = CredentialMapper.toJWTCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + jwtCredentials = filterJwtByMetadata(credentialQuery.getMeta(), jwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + jwtCredentials = jwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + jwtCredentials = evaluateJwtCredentialsClaimQuery(cq, jwtCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, jwtCredentials, DCQLEvaluator::evaluateJwtCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.JWT_VC_JSON, jwtCredentials); + } + + private static List evaluateForLdpVC(CredentialQuery credentialQuery, List credentialsList) { + List ldpCredentials = CredentialMapper.toLdpCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + ldpCredentials = filterLdpByMetadata(credentialQuery.getMeta(), ldpCredentials); + } + + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + ldpCredentials = evaluateLdpCredentialsClaimQuery(cq, ldpCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, ldpCredentials, DCQLEvaluator::evaluateLdpCredentialsClaimQuery); + } + + return CredentialMapper.toCredentials(CredentialFormat.LDP_VC, ldpCredentials); + } + + private static List evaluateForMDoc(CredentialQuery credentialQuery, List credentialsList) { + List mDocCredentials = CredentialMapper.toMDocCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + mDocCredentials = filterMDocByMetadata(credentialQuery.getMeta(), mDocCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + mDocCredentials = mDocCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForMDocCredential(taq, credential)) + .toList(); + } + } + translateMDocQueries(credentialQuery); + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + mDocCredentials = evaluateMDocCredentialsClaimQuery(cq, mDocCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, mDocCredentials, DCQLEvaluator::evaluateMDocCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.MSO_MDOC, mDocCredentials); + } + + private static List evaluateSdJwtForClaimSet(CredentialQuery credentialQuery, List sdJwtCredentials) { + Map claimsQueryMap = new HashMap<>(); + credentialQuery.getClaims() + .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); + + for (List claimSet : credentialQuery.getClaimSets()) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set disclosures = new HashSet<>(); + for (String claimId : claimSet) { + ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); + disclosures.addAll(new HashSet<>( + ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, credential) + .map(SdJwtCredential::getDisclosures) + .orElse(new ArrayList<>()))); + } + if (!disclosures.isEmpty()) { + disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(disclosures))); + } + } + + if (!disclosedCredentials.isEmpty()) { + return CredentialMapper.toCredentials(credentialQuery.getFormat(), disclosedCredentials); + } + } + return List.of(); + } + + // The method returns the first claim set that is fullfilled. It can contain multiple credentials, that would + // fulfill the set individually, leaving the choice of what to share to the upstream. + private static List evaluateForClaimSet(CredentialQuery credentialQuery, List initialCredentials, BiFunction, List> evaluationFunction) { + Map claimsQueryMap = new HashMap<>(); + credentialQuery.getClaims() + .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); + + for (List claimSet : credentialQuery.getClaimSets()) { + List credentialsForClaimSet = new ArrayList<>(initialCredentials); + for (String claimId : claimSet) { + ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); + credentialsForClaimSet = evaluationFunction.apply(claimsQuery, credentialsForClaimSet); + } + if (!credentialsForClaimSet.isEmpty()) { + return CredentialMapper.toCredentials(credentialQuery.getFormat(), credentialsForClaimSet); + } + } + return List.of(); + } + + private static List evaluateSdJwtCredentialsQuery(CredentialQuery credentialQuery, List sdJwtCredentials) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set selectedDisclosures = credentialQuery.getClaims() + .stream() + .map(cq -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(SdJwtCredential::getDisclosures) + .flatMap(List::stream) + .collect(Collectors.toSet()); + disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); + } + return disclosedCredentials; + } + + private static List evaluateSdJwtCredentialsClaimQuery(ClaimsQuery cq, List sdJwtCredentials) { + return sdJwtCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { + return ldpCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static List evaluateJwtCredentialsClaimQuery(ClaimsQuery cq, List jwtCredentials) { + return jwtCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static List evaluateMDocCredentialsClaimQuery(ClaimsQuery cq, List mDocCredentials) { + return mDocCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForMDocCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static boolean isMDocClaimsQuery(ClaimsQuery claimsQuery) { + if ((claimsQuery.getNamespace() != null && claimsQuery.getClaimName() == null) || (claimsQuery.getNamespace() == null && claimsQuery.getClaimName() != null)) { + throw new IllegalArgumentException("When a namespace or claim_name is set, the other parameter is mandatory."); + } + return claimsQuery.getIntent_to_retain() != null || claimsQuery.getNamespace() != null; + } + + private static CredentialQuery translateMDocQueries(CredentialQuery credentialQuery) { + if (credentialQuery.getClaims() == null) { + return credentialQuery; + } + credentialQuery.getClaims() + .forEach(cq -> { + if (isMDocClaimsQuery(cq) && cq.getNamespace() != null) { + cq.setPath(List.of(MDOC_NAMESPACE_KEY, cq.getNamespace(), cq.getClaimName())); + } else { + cq.getPath().addFirst(MDOC_NAMESPACE_KEY); + } + }); + return credentialQuery; + } + + private static List filterByFormat(CredentialFormat credentialFormat, List credentialsList) { + return credentialsList.stream() + .filter(c -> c.getCredentialFormat() == credentialFormat) + .toList(); + } + + private static List filterLdpByMetadata(Map metaData, List credentialsList) { + W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(ldpCredential -> + w3CMetaData.getTypeValues() + .stream() + .anyMatch(metaTypes -> new HashSet<>(ldpCredential.getType()).containsAll(metaTypes))) + .toList(); + } + + private static List filterSdJwtByMetadata(Map metaData, List credentialsList) { + JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(sdJwtCredential -> jwtMetaData.getVctValues().contains(sdJwtCredential.getVct())) + .toList(); + } + + private static List filterJwtByMetadata(Map metaData, List credentialsList) { + JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(jwtCredential -> jwtMetaData.getVctValues().contains(jwtCredential.getVct())) + .toList(); + } + + private static List filterMDocByMetadata(Map metaData, List credentialsList) { + MDocMetaData mDocMetaData = MDocMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(mDocCredential -> mDocCredential.getDocType().equals(mDocMetaData.getDocType())) + .toList(); + } + + private static boolean containsClaims(CredentialQuery credentialQuery) { + return credentialQuery.getClaims() != null && !credentialQuery.getClaims().isEmpty(); + } + + private static boolean containsClaimSets(CredentialQuery credentialQuery) { + return credentialQuery.getClaimSets() != null && !credentialQuery.getClaimSets().isEmpty(); + } + + private static boolean containsMeta(CredentialQuery credentialQuery) { + return credentialQuery.getMeta() != null && !credentialQuery.getMeta().isEmpty(); + } + + private static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { + return credentialQuery.getTrustedAuthorities() != null && !credentialQuery.getTrustedAuthorities().isEmpty(); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/TrustedAuthoritiesEvaluator.java b/src/main/java/io/github/wistefan/dcql/TrustedAuthoritiesEvaluator.java new file mode 100644 index 0000000..f904c07 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/TrustedAuthoritiesEvaluator.java @@ -0,0 +1,89 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.TrustedAuthorityQuery; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; +import java.util.Optional; + +@Slf4j +public class TrustedAuthoritiesEvaluator { + + private static final String AKI_EXTENSION = "2.5.29.35"; + + public static boolean evaluateQueryForMDocCredential(TrustedAuthorityQuery query, MDocCredential credential) { + return switch (query.getType()) { + case AKI -> isInChain(credential.getHeaders().getX5Chain(), query.getValues()); + case ETSI_TL -> isInEtsiTl(credential.getHeaders().getX5Chain(), query.getValues()); + case OPENID_FEDERATION -> isInOpenIdFederation(query.getValues()); + }; + } + + public static boolean evaluateQueryForSDJwtCredential(TrustedAuthorityQuery query, SdJwtCredential credential) { + return evaluateQueryForJwtCredential(query, credential.getJwtCredential()); + } + + public static boolean evaluateQueryForJwtCredential(TrustedAuthorityQuery query, JwtCredential credential) { + return switch (query.getType()) { + case AKI -> isInChain(credential.getX5Chain(), query.getValues()); + case ETSI_TL -> isInEtsiTl(credential.getX5Chain(), query.getValues()); + case OPENID_FEDERATION -> isInOpenIdFederation(query.getValues()); + }; + } + + + // ---- OpenID Federation ---- + private static boolean isInOpenIdFederation(List federationValues) { + throw new UnsupportedOperationException("Querying for OpenId Federation Trust Authorities is not yet supported."); + } + + // ---- ETSI TL ---- + private static boolean isInEtsiTl(List x5chain, List etsiTls) { + throw new UnsupportedOperationException("Querying for etsi-tl is not supported at the moment."); + } + + // ---- AKI ---- + + private static boolean isInChain(List x5chain, List akiValues) { + return x5chain.stream() + .map(TrustedAuthoritiesEvaluator::getAuthorityKeyIdentifier) + .filter(Optional::isPresent) + .map(Optional::get) + .map(byteArray -> Base64.getUrlEncoder().encodeToString(byteArray)) + .anyMatch(akiValues::contains); + } + + private static List decodeAki(List akiValues) { + return akiValues.stream() + .map(v -> Base64.getUrlDecoder().decode(v)) + .toList(); + } + + public static Optional getAuthorityKeyIdentifier(X509Certificate certificate) { + + byte[] extValue = certificate.getExtensionValue(AKI_EXTENSION); + if (extValue == null) { + + return Optional.empty(); + } + ASN1OctetString akiOctet = ASN1OctetString.getInstance(extValue); + ASN1Primitive akiObj = null; + try { + akiObj = ASN1Primitive.fromByteArray(akiOctet.getOctets()); + } catch (IOException e) { + log.debug("Certificate does not contain a valid aki.", e); + return Optional.empty(); + } + AuthorityKeyIdentifier aki = AuthorityKeyIdentifier.getInstance(akiObj); + return Optional.ofNullable(aki.getKeyIdentifier()); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java b/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java new file mode 100644 index 0000000..7ca8fdb --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java @@ -0,0 +1,60 @@ + +package io.github.wistefan.dcql.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class ClaimsQuery { + + public ClaimsQuery(String id, List path, List values) { + this.id = id; + this.path = path; + this.values = values; + } + + /** + * REQUIRED if claim_sets is present in the Credential Query; OPTIONAL otherwise. A string identifying the + * particular claim. The value MUST be a non-empty string consisting of alphanumeric, underscore (_), or hyphen (-) + * characters. Within the particular claims array, the same id MUST NOT be present more than once. + */ + private String id; + + /** + * The value MUST be a non-empty array representing a claims path pointer that specifies the path to a claim within + * the Credential. + */ + private List path; + + /** + * A non-empty array of strings, integers or boolean values that specifies the expected values of the claim. If the + * values property is present, the Wallet SHOULD return the claim only if the type and value of the claim both match + * exactly for at least one of the elements in the array. + */ + private List values; + + // ---- MDoc Specific parameters ---- + + /** + * MDoc specific parameter. The flag can be set to inform that the reader wishes to keep(store) the data. In case of + * false, its data is only used to be dispalyed and verified. + */ + private Boolean intent_to_retain; + + /** + * Refers to a namespace inside an mdoc + */ + private String namespace; + + /** + * Identifier for the data-element in the namespace + */ + @JsonProperty("claim_name") + private String claimName; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/Credential.java b/src/main/java/io/github/wistefan/dcql/model/Credential.java new file mode 100644 index 0000000..07ce200 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/Credential.java @@ -0,0 +1,14 @@ +package io.github.wistefan.dcql.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Credential { + + private CredentialFormat credentialFormat; + private Object rawCredential; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java new file mode 100644 index 0000000..5e7f7ef --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java @@ -0,0 +1,37 @@ +package io.github.wistefan.dcql.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +import java.util.Arrays; + +public enum CredentialFormat { + + MSO_MDOC("mso_mdoc"), + VC_SD_JWT("vc+sd-jwt"), + DC_SD_JWT("dc+sd-jwt"), + LDP_VC("ldp_vc"), + JWT_VC_JSON("jwt_vc_json"); + + @Getter + private final String value; + + CredentialFormat(String value) { + this.value = value; + } + + @JsonCreator + public static CredentialFormat fromValue(String value) { + return Arrays.stream(values()) + .filter(eV -> eV.getValue().equals(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("Unknown value %s.", value))); + } + + @JsonValue + public String getValue() { + return value; + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java new file mode 100644 index 0000000..5ad1774 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java @@ -0,0 +1,69 @@ + +package io.github.wistefan.dcql.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * A Credential Query is an object representing a request for a presentation of one or more matching Credentials. + */ +@Data +public class CredentialQuery { + + /** + * A string identifying the Credential in the response and, if provided, the constraints in credential_sets. The + * value MUST be a non-empty string consisting of alphanumeric, underscore (_), or hyphen (-) characters. Within the + * Authorization Request, the same id MUST NOT be present more than once. + */ + private String id; + + /** + * A string that specifies the format of the requested Credential. + */ + private CredentialFormat format; + + /** + * A boolean which indicates whether multiple Credentials can be returned for this Credential Query. If omitted, the + * default value is false. If empty, no specific constraints are placed on the metadata or validity of the requested Credential. + */ + private Boolean multiple = false; + + /** + * A non-empty array of objects that specifies claims in the requested Credential. Verifiers MUST NOT point to the + * same claim more than once in a single query. Wallets SHOULD ignore such duplicate claim queries. + */ + private List claims; + + /** + * An object defining additional properties requested by the Verifier that apply to the metadata and validity data + * of the Credential. The properties of this object are defined per Credential Format. If empty, no specific + * constraints are placed on the metadata or validity of the requested Credential. + */ + private Map meta; + + /** + * A boolean which indicates whether the Verifier requires a Cryptographic Holder Binding proof. The default value + * is true, i.e., a Verifiable Presentation with Cryptographic Holder Binding is required. If set to false, the + * Verifier accepts a Credential without Cryptographic Holder Binding proof. + */ + @JsonProperty("require_cryptographic_holder_binding") + private Boolean requireCryptographicHolderBinding; + + /** + * A non-empty array containing arrays of identifiers for elements in claims that specifies which combinations of + * claims for the Credential are requested. + */ + @JsonProperty("claim_sets") + private List> claimSets; + + /** + * A non-empty array of objects that specifies expected authorities or trust frameworks that certify Issuers, that + * the Verifier will accept. Every Credential returned by the Wallet SHOULD match at least one of the conditions + * present in the corresponding trusted_authorities array if present. + */ + @JsonProperty("trusted_authorities") + private List trustedAuthorities; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java new file mode 100644 index 0000000..cdedc9c --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java @@ -0,0 +1,27 @@ + +package io.github.wistefan.dcql.model; + +import lombok.Data; + +import java.util.List; + +/** + * A Credential Set Query is an object representing a request for one or more Credentials to satisfy a particular use + * case with the Verifier. + */ +@Data +public class CredentialSetQuery{ + + /** + * A non-empty array, where each value in the array is a list of Credential Query identifiers representing one set + * of Credentials that satisfies the use case. The value of each element in the options array is a non-empty array + * of identifiers which reference elements in credentials. + */ + private List> options; + + /** + * A boolean which indicates whether this set of Credentials is required to satisfy the particular use case at the + * Verifier. + */ + private Boolean required = true; +} \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java new file mode 100644 index 0000000..e0cad7b --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java @@ -0,0 +1,27 @@ + +package io.github.wistefan.dcql.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * A JSON-encoded query that allows the Verifier to request presentations that match the query. + */ +@Data +public class DcqlQuery { + + /** + * A non-empty array of Credential Queries that specify the requested Credentials. + */ + private List credentials; + + /** + * A non-empty array of Credential Set Queries that specifies additional constraints on which of the requested + * Credentials to return. + */ + @JsonProperty("credential_sets") + private List credentialSets; + +} diff --git a/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java b/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java new file mode 100644 index 0000000..f71dd63 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java @@ -0,0 +1,30 @@ +package io.github.wistefan.dcql.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.*; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JwtMetaData { + private static final String VCT_VALUES_KEY = "vct_values"; + + private Set vctValues; + + public static JwtMetaData fromMeta(Map metaData) { + if (metaData.containsKey(VCT_VALUES_KEY) && metaData.get(VCT_VALUES_KEY) instanceof List vctValues) { + List vctStrings = vctValues.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (vctValues.size() != vctStrings.size()) { + throw new IllegalArgumentException(String.format("The vct_values %s contain invalid values.", vctValues)); + } + return new JwtMetaData(new HashSet<>(vctStrings)); + } + throw new IllegalArgumentException(String.format("Given metaData %s is not sdJwt-metadata.", metaData)); + } +} \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java b/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java new file mode 100644 index 0000000..619bd34 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java @@ -0,0 +1,24 @@ +package io.github.wistefan.dcql.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class MDocMetaData { + + private static final String DOCTYPE_KEY = "doctype_value"; + + private String docType; + + public static MDocMetaData fromMeta(Map metaData) { + if (metaData.containsKey(DOCTYPE_KEY) && metaData.get(DOCTYPE_KEY) instanceof String docType) { + return new MDocMetaData(docType); + } + throw new IllegalArgumentException(String.format("Given metaData %s is not mDoc-metadata.", metaData)); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java new file mode 100644 index 0000000..c2a1284 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java @@ -0,0 +1,28 @@ + +package io.github.wistefan.dcql.model; + +import lombok.Data; + +import java.util.List; + +/** + * An object representing information that helps to identify an authority or the trust framework that certifies Issuers. + * A Credential is identified as a match to a Trusted Authorities Query if it matches with one of the provided values in + * one of the provided types. + */ +@Data +public class TrustedAuthorityQuery { + + /** + * A string uniquely identifying the type of information about the issuer trust framework. Types defined by this + * specification are listed below. + */ + private TrustedAuthorityType type; + + /** + * A non-empty array of strings, where each string (value) contains information specific to the used Trusted + * Authorities Query type that allows the identification of an issuer, a trust framework, or a federation that an + * issuer belongs to. + */ + private List values; +} \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java new file mode 100644 index 0000000..7c28424 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java @@ -0,0 +1,34 @@ +package io.github.wistefan.dcql.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; + +import java.util.Arrays; + +public enum TrustedAuthorityType { + + AKI("aki"), + ETSI_TL("etsi_tl"), + OPENID_FEDERATION("openid_federation"); + + @Getter + private final String value; + + TrustedAuthorityType(String value) { + this.value = value; + } + + @JsonCreator + public static TrustedAuthorityType fromValue(String value) { + return Arrays.stream(values()) + .filter(eV -> eV.getValue().equals(value)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("Unknown value %s.", value))); + } + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java b/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java new file mode 100644 index 0000000..7170fb7 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java @@ -0,0 +1,45 @@ +package io.github.wistefan.dcql.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class W3CMetaData { + + private static final String TYPE_VALUES_KEY = "type_values"; + + private List> typeValues; + + public static W3CMetaData fromMeta(Map metaData) { + if (metaData.containsKey(TYPE_VALUES_KEY) && metaData.get(TYPE_VALUES_KEY) instanceof List typeValues) { + + List> typeValuesStrings = typeValues.stream() + .filter(List.class::isInstance) + .map(l -> mapToStringList((List) l)) + .toList(); + if (typeValuesStrings.size() != typeValues.size()) { + throw new IllegalArgumentException(String.format("The type_values %s contain invalid values.", typeValues)); + } + return new W3CMetaData(typeValues); + } + throw new IllegalArgumentException(String.format("Given metaData %s is not w3c-metadata.", metaData)); + } + + private static List mapToStringList(List listToMap) { + List stringList = listToMap.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (stringList.size() != listToMap.size()) { + throw new IllegalArgumentException("Not all list entries are strings"); + } + return stringList; + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java new file mode 100644 index 0000000..703f095 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java @@ -0,0 +1,16 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@Data +@NoArgsConstructor +@EqualsAndHashCode +public class Disclosure { + private String hash; + private String claim; + private Object value; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java new file mode 100644 index 0000000..cab10bc --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java @@ -0,0 +1,63 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JwtCredential { + + private static final String VCT_KEY = "vct"; + private static final String TYPE_KEY = "type"; + private static final String X5C_KEY = "x5c"; + + private Map headers; + private Map payload; + private String signature; + + public List getX5Chain() { + if (headers.containsKey(X5C_KEY) && headers.get(X5C_KEY) instanceof List x5Chain) { + List x509Certificates = x5Chain.stream() + .filter(X509Certificate.class::isInstance) + .map(X509Certificate.class::cast) + .toList(); + if (x5Chain.size() != x509Certificates.size()) { + throw new IllegalArgumentException("The x5c header contains invalid values."); + } + return x509Certificates; + } + // a x5c-header is not mandatory, thus an empty list is completely valid. + return List.of(); + } + + public List getType() { + if (payload.containsKey(TYPE_KEY)) { + if (payload.get(TYPE_KEY) instanceof String typeString) { + return List.of(typeString); + } else if (payload.get(TYPE_KEY) instanceof List typeList) { + List typeStrings = typeList.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (typeStrings.size() == typeList.size()) { + return typeStrings; + } + } + } + throw new IllegalArgumentException("The type field contains invalid entries."); + } + + public String getVct() { + if (payload.containsKey(VCT_KEY) && payload.get(VCT_KEY) instanceof String vctValue) { + return vctValue; + } + throw new IllegalArgumentException("Invalid credential. Does not contain a valid vct."); + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java new file mode 100644 index 0000000..f7c45af --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java @@ -0,0 +1,32 @@ +package io.github.wistefan.dcql.model.credential; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LdpCredential extends HashMap { + + private static final String TYPE_KEY = "type"; + + public LdpCredential(Map m) { + super(m); + } + + public List getType() { + if (this.containsKey(TYPE_KEY)) { + if (this.get(TYPE_KEY) instanceof String typeString) { + return List.of(typeString); + } else if (this.get(TYPE_KEY) instanceof List typeList) { + List typeStrings = typeList.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (typeStrings.size() == typeList.size()) { + return typeStrings; + } + } + } + throw new IllegalArgumentException("The type field contains invalid entries."); + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java new file mode 100644 index 0000000..1e4fba0 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java @@ -0,0 +1,25 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +@AllArgsConstructor +@Data +@NoArgsConstructor +public class MDocCredential { + + private static final String DOC_TYPE_KEY = "docType"; + + private MDocHeaders headers; + private Map payload; + + public String getDocType() { + if (payload.containsKey(DOC_TYPE_KEY) && payload.get(DOC_TYPE_KEY) instanceof String docType) { + return docType; + } + throw new IllegalArgumentException("The credential does not contain a valid docType."); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java b/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java new file mode 100644 index 0000000..dba9a3d --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java @@ -0,0 +1,17 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.security.cert.X509Certificate; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MDocHeaders { + + private String alg; + private List x5Chain; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java new file mode 100644 index 0000000..6ebed83 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java @@ -0,0 +1,24 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@Data +@NoArgsConstructor +public class SdJwtCredential { + + private JwtCredential jwtCredential; + private List disclosures; + + public String getVct() { + return jwtCredential.getVct(); + } + + public List getType() { + return jwtCredential.getType(); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java new file mode 100644 index 0000000..b7aa59d --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java @@ -0,0 +1,17 @@ +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ClaimEvaluationResult( + boolean success, + @JsonProperty("claim_index") int claimIndex, + @JsonProperty("claim_id") String claimId, + Map output, + Map> issues +) { +} diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java new file mode 100644 index 0000000..e286fdd --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java @@ -0,0 +1,18 @@ +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ClaimSetResult( + boolean success, + @JsonProperty("claim_set_index") Integer claimSetIndex, + Map output, + @JsonProperty("valid_claim_indexes") List validClaimIndexes, + @JsonProperty("failed_claim_indexes") List failedClaimIndexes, + Map> issues +) { +} \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java new file mode 100644 index 0000000..81c2cf4 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java @@ -0,0 +1,19 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ClaimsResult( + boolean success, + @JsonProperty("valid_claim_sets") List validClaimSets, + @JsonProperty("failed_claim_sets") List failedClaimSets, + @JsonProperty("valid_claims") List validClaims, + @JsonProperty("failed_claims") List failedClaims +) {} + + + diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java new file mode 100644 index 0000000..bcf4aa0 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java @@ -0,0 +1,14 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CredentialEvaluationResult( + boolean success, + @JsonProperty("input_credential_index") int inputCredentialIndex, + MetaResult meta, + ClaimsResult claims, + @JsonProperty("trusted_authorities") TrustedAuthoritiesResult trustedAuthorities +) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java b/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java new file mode 100644 index 0000000..3621148 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java @@ -0,0 +1,14 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CredentialMatch( + boolean success, + @JsonProperty("credential_query_id") String credentialQueryId, + @JsonProperty("valid_credentials") List validCredentials, + @JsonProperty("failed_credentials") List failedCredentials +) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java b/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java new file mode 100644 index 0000000..3b65c4d --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java @@ -0,0 +1,14 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record CredentialSetResult( + String purpose, + Boolean required, + List> options, + @JsonProperty("matching_options") List> matchingOptions +) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/MetaResult.java b/src/main/java/io/github/wistefan/dcql/result/MetaResult.java new file mode 100644 index 0000000..ff199fc --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/MetaResult.java @@ -0,0 +1,14 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record MetaResult( + boolean success, + Map output, + Map> issues +) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/QueryResult.java b/src/main/java/io/github/wistefan/dcql/result/QueryResult.java new file mode 100644 index 0000000..68fb4cd --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/QueryResult.java @@ -0,0 +1,17 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +// Using @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields from JSON output, matching TS behavior + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record QueryResult( + boolean canBeSatisfied, + @JsonProperty("credential_matches") Map credentialMatches, + @JsonProperty("credential_sets") List credentialSets +) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java new file mode 100644 index 0000000..021ac8f --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java @@ -0,0 +1,15 @@ + +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record TrustedAuthoritiesResult( + boolean success, + @JsonProperty("valid_trusted_authority") TrustedAuthorityEvaluationResult validTrustedAuthority, + @JsonProperty("failed_trusted_authorities") List failedTrustedAuthorities +) {} + diff --git a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java new file mode 100644 index 0000000..d73efd9 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java @@ -0,0 +1,16 @@ +package io.github.wistefan.dcql.result; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record TrustedAuthorityEvaluationResult( + boolean success, + @JsonProperty("trusted_authority_index") int trustedAuthorityIndex, + Map output, + Map> issues +) { +} diff --git a/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java b/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java new file mode 100644 index 0000000..4d07649 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java @@ -0,0 +1,77 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.ClaimsQuery; +import io.github.wistefan.dcql.model.credential.Disclosure; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ClaimsEvaluatorTest { + + @ParameterizedTest + @MethodSource("jwtArgs") + public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map credential, boolean expectedResult) { + JwtCredential jwtCredential = new JwtCredential(null, credential, null); + assertEquals(expectedResult, ClaimsEvaluator.evaluateClaimsForJwtCredential(claimsQuery, jwtCredential).isPresent()); + } + + @ParameterizedTest + @MethodSource("sdJwtArgs") + public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map payload, List disclosures, Optional> expectedDisclosures) { + + SdJwtCredential sdJwtCredential = new SdJwtCredential(new JwtCredential(null, payload, null), disclosures); + Optional optionalSdJwtCredential = ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, sdJwtCredential); + + assertEquals(expectedDisclosures.isPresent(), optionalSdJwtCredential.isPresent()); + expectedDisclosures.ifPresent(disclosureList -> assertEquals(disclosureList, optionalSdJwtCredential.get().getDisclosures())); + } + + public static Stream sdJwtArgs() { + return Stream.of( + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), null), + Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), + List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), + Optional.of(List.of(new Disclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), List.of("b")), + Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), + List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), + Optional.of(List.of(new Disclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), List.of("c")), + Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), + List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), + Optional.empty()), + Arguments.of( + new ClaimsQuery("id", List.of("test","e"), List.of("f")), + Map.of("_sd", List.of("hash-a", "hash-b"), "test",Map.of("_sd", List.of("hash-c"))), + List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d"), new Disclosure("hash-c", "e", "f")), + Optional.of(List.of(new Disclosure("hash-c", "e", "f")))) + ); + } + + public static Stream jwtArgs() { + List nullList = new ArrayList<>(); + nullList.add("test"); + nullList.add(null); + nullList.add("a"); + return Stream.of( + Arguments.of(new ClaimsQuery("id", nullList, null), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), true), + Arguments.of(new ClaimsQuery("id", nullList, List.of("c")), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), false), + Arguments.of(new ClaimsQuery("id", List.of("test", "a"), null), Map.of("test", Map.of("a", "b", "c", "d")), true), + Arguments.of(new ClaimsQuery("id", List.of("test", "d"), null), Map.of("test", Map.of("a", "b", "c", "d")), false), + Arguments.of(new ClaimsQuery("id", List.of("test", "a"), List.of("b")), Map.of("test", Map.of("a", "b", "c", "d")), true) + ); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java new file mode 100644 index 0000000..169e5f8 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java @@ -0,0 +1,245 @@ +package io.github.wistefan.dcql.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.DcqlQuery; +import io.github.wistefan.dcql.model.credential.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class DcqlClaimSetQueryTest extends DcqlTest { + + private static final String MDOC_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, + { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "claim_sets": [ + ["b","c"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final Credential MDOC_MVRC_FULL = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), + "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_HOLDER = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_LAST_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.18013.5.1", Map.of("last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + + private static final String SD_JWT_QUERY_ADDRESS = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential"] }, + "claims": [ + { "id": "a", "path": ["address","street_address"] }, + { "id": "b", "path": ["street_address"] } + ], + "claim_sets": [ + ["b"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_QUERY_ALTERNATIVES = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential","https://credentials.example.com/name_credential"] }, + "claims": [ + { "id": "a", "path": ["address","street_address"] }, + { "id": "b", "path": ["street_address"] }, + { "id": "c", "path": ["first_name"] }, + { "id": "d", "path": ["last_name"] } + ], + "claim_sets": [ + ["c","d"], + ["b"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + private static final Credential SD_JWT_VC_FULL = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( + new JwtCredential(null, + Map.of( + "vct", "https://credentials.example.com/identity_credential", + "name", Map.of("_sd", List.of("hash-b", "hash-c")), + "address", Map.of("_sd", List.of("hash-a", "hash-x")), + "cryptographic_holder_binding", false), null), + List.of(new Disclosure("hash-a", "street_address", "42 Market Street"), + new Disclosure("hash-b", "first_name", "Arthur"), + new Disclosure("hash-c", "last_name", "Dent")) + )); + + private static final Credential SD_JWT_VC_ADDRESS = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( + new JwtCredential(null, + Map.of( + "vct", "https://credentials.example.com/address_credential", + "_sd", List.of("hash-a", "hash-x"), + "cryptographic_holder_binding", false), null), + List.of(new Disclosure("hash-a", "street_address", "42 Market Street")) + )); + + private static final Credential SD_JWT_VC_NAME = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( + new JwtCredential(null, + Map.of( + "vct", "https://credentials.example.com/name_credential", + "_sd", List.of("hash-b", "hash-c"), + "cryptographic_holder_binding", false), null), + List.of(new Disclosure("hash-b", "first_name", "Arthur"), + new Disclosure("hash-c", "last_name", "Dent")) + )); + + @Test + @DisplayName("sd-jwt query get alternative") + void sdJwtQueryGetAlternative() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + + assertEquals(1, credentialsResult.size()); + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + + @Test + @DisplayName("sd-jwt query get for name") + void sdJwtQueryForName() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + + assertEquals(1, credentialsResult.size()); + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(2, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("sd-jwt query get for street_address within full") + void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + + assertEquals(1, credentialsResult.size()); + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("sd-jwt query get for street_address") + void sdJwtQueryForStreetAddress() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + + assertEquals(1, credentialsResult.size()); + + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("mdoc mvrc query get full doc") + void mdocMvrcQueryFullDocSet() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); + + assertEquals(1, credentialsResult.size()); + assertEquals(credentialsResult.get(0), MDOC_MVRC_FULL); + } + + @Test + @DisplayName("mdoc mvrc query get second set") + void mdocMvrcQuerySecondSet() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); + + assertEquals(1, credentialsResult.size()); + assertEquals(credentialsResult.get(0), MDOC_MVRC_HOLDER); + } + + @Test + @DisplayName("mdoc mvrc query gets the fullfilling credentials.") + void mdocMvrcQueryOnlyOne() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + + assertEquals(2, credentialsResult.size()); + assertTrue(credentialsResult.contains(MDOC_MVRC_NAME)); + assertTrue(credentialsResult.contains(MDOC_MVRC_FULL)); + } +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java new file mode 100644 index 0000000..eaa1a00 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java @@ -0,0 +1,172 @@ + +package io.github.wistefan.dcql.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.DcqlQuery; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import io.github.wistefan.dcql.result.CredentialSetResult; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class DcqlQueryComplexTest extends DcqlTest { + + // --- Test Data --- + + private static final String COMPLEX_MDOC_QUERY = """ + { + "credentials": [ + { + "id": "mdl-id", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, + "claims": [ + { "id": "given_name", "namespace": "org.iso.18013.5.1", "claim_name": "given_name" }, + { "id": "family_name", "namespace": "org.iso.18013.5.1", "claim_name": "family_name" }, + { "id": "portrait", "namespace": "org.iso.18013.5.1", "claim_name": "portrait" } + ] + }, + { + "id": "mdl-address", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, + "claims": [ + { "id": "resident_address", "path": ["org.iso.18013.5.1", "resident_address"], "intent_to_retain": false }, + { "id": "resident_country", "path": ["org.iso.18013.5.1", "resident_country"], "intent_to_retain": true } + ] + }, + { + "id": "photo_card-id", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.23220.photoid.1" }, + "claims": [ + { "id": "given_name", "path": ["org.iso.23220.1", "given_name"] }, + { "id": "family_name", "path": ["org.iso.23220.1", "family_name"] }, + { "id": "portrait", "path": ["org.iso.23220.1", "portrait"] } + ] + }, + { + "id": "photo_card-address", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.23220.photoid.1" }, + "claims": [ + { "id": "resident_address", "path": ["org.iso.23220.1", "resident_address"] }, + { "id": "resident_country", "path": ["org.iso.23220.1", "resident_country"] } + ] + } + ], + "credential_sets": [ + { "purpose": "Identification", "options": [["mdl-id"], ["photo_card-id"]] }, + { "purpose": "Proof of address", "required": false, "options": [["mdl-address"], ["photo_card-address"]] } + ] + } + """; + + private static final Credential MDOC_MDL_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.18013.5.1.mDL", + "namespaces", Map.of("org.iso.18013.5.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MDL_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.18013.5.1.mDL", + "namespaces", Map.of("org.iso.18013.5.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_PHOTO_CARD_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.23220.photoid.1", + "namespaces", Map.of("org.iso.23220.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_PHOTO_CARD_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.23220.photoid.1", + "namespaces", Map.of("org.iso.23220.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_EXAMPLE = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "credential_format", "mso_mdoc", + "docType", "example_doctype", + "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), + "cryptographic_holder_binding", true + ))); + + private static final Credential SD_JWT_VC_EXAMPLE = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential( + new JwtCredential(null, Map.of( + "credential_format", "vc+sd-jwt", + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of( + "first_name", "Arthur", + "last_name", "Dent", + "address", Map.of("street_address", "42 Market Street", "locality", "Milliways", "postal_code", "12345"), + "degrees", List.of( + Map.of("type", "Bachelor of Science", "university", "University of Betelgeuse"), + Map.of("type", "Master of Science", "university", "University of Betelgeuse") + ), + "nationalities", List.of("British", "Betelgeusian") + ), + "cryptographic_holder_binding", true + ), null), List.of())); + + + @Test + @DisplayName("fails with no credentials") + void failsWithNoCredentials() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); + + assertEquals(0, credentialsResult.size()); + } + + @Test + @DisplayName("fails with credentials that do not satisfy a required claim_set") + void failsWithCredentialsThatDoNotSatisfyARequiredClaimSet() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); + + assertEquals(0, credentialsResult.size()); + } + + @Test + @DisplayName("succeeds if all credentials are present") + void succeedsIfAllCredentialsArePresent() throws JsonProcessingException { + List expectedCredentials = List.of(MDOC_MDL_ID, MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ID, MDOC_PHOTO_CARD_ADDRESS); + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + MDOC_MDL_ID, + MDOC_MDL_ADDRESS, + MDOC_PHOTO_CARD_ID, + MDOC_PHOTO_CARD_ADDRESS, + MDOC_EXAMPLE, + SD_JWT_VC_EXAMPLE)); + + assertEquals(4, credentialsResult.size(), "Only the MDoc Credentials of th reight type should be included."); + + expectedCredentials.forEach( + ec -> assertTrue(credentialsResult.contains(ec)) + ); + } + +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java new file mode 100644 index 0000000..38bf969 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java @@ -0,0 +1,262 @@ + +package io.github.wistefan.dcql.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.DcqlQuery; +import io.github.wistefan.dcql.model.credential.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class DcqlQueryTest extends DcqlTest { + + private static final String MDOC_MVRC_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"mso_mdoc\",\n" + + " \"meta\": { \"doctype_value\": \"org.iso.7367.1.mVRC\" },\n" + + " \"require_cryptographic_holder_binding\": true,\n" + + " \"claims\": [\n" + + " { \"path\": [\"org.iso.7367.1\", \"vehicle_holder\"], \"intent_to_retain\": false },\n" + + " { \"path\": [\"org.iso.18013.5.1\", \"first_name\"], \"intent_to_retain\": true }\n" + + " ],\n" + + " \"trusted_authorities\": [\n" + + " { \"type\": \"aki\", \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"] }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final String MDOC_NAMESPACE_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential EXAMPLE_MDOC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "docType", "example_doctype", + "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), + "authority", Map.of("type", "aki", "values", List.of("something")), + "cryptographic_holder_binding", true + ))); + + private static final Credential EXAMPLE_SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( + new JwtCredential(null, + Map.of( + "vct", "https://credentials.example.com/identity_credential", + "_sd", List.of("hash-b", "hash-c"), + "address", Map.of("_sd", List.of("hash-a", "hash-x")), + "cryptographic_holder_binding", false), null), + List.of(new Disclosure("hash-a", "street_address", "42 Market Street"), + new Disclosure("hash-b", "first_name", "Arthur"), + new Disclosure("hash-c", "last_name", "Dent")) + )); + + private static final Credential EXAMPLE_W3C_LDP_VC = new Credential(CredentialFormat.LDP_VC, new LdpCredential(Map.of( + "type", List.of("https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"), + "credentialSubject", Map.of("first_name", "Arthur", "last_name", "Dent", "address", Map.of("street_address", "42 Market Street")), + "cryptographic_holder_binding", false + ))); + + + private static final String SD_JWT_VC_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "multiple": true, + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "multiple": true, + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String W3C_LDP_VC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "ldp_vc", + "meta": { + "type_values": [ + ["https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"], + ["https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#UniversityDegreeCredential"] + ] + }, + "claims": [ { "path": ["credentialSubject", "last_name"] }, { "path": ["credentialSubject", "first_name"] }, { "path": ["credentialSubject", "address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + + @Test + @DisplayName("mdoc mvrc query fails with invalid mdoc") + void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); + + assertEquals(0, credentialsResult.size()); + } + + @Test + @DisplayName("mdoc mvrc example with multiple credentials succeeds") + void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); + + assertEquals(1, credentialsResult.size()); + assertEquals(MDOC_MVRC, credentialsResult.get(0)); + } + + @Test + @DisplayName("w3cLdpVc example succeeds") + void w3cLdpVcExampleSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); + + assertEquals(1, credentialsResult.size()); + } + + @Test + @DisplayName("w3cLdpVc query fails with invalid type values") + void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertTrue(credentialsResult.isEmpty()); + } + + @Test + @DisplayName("mdocMvrc example using namespaces succeeds") + void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_NAMESPACE_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertEquals(1, credentialsResult.size()); + } + + @Test + @DisplayName("sdJwtVc example with multiple credentials succeeds") + void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); + + assertFalse(credentialsResult.isEmpty()); + assertEquals(1, credentialsResult.size()); + Credential theCredential = credentialsResult.get(0); + assertEquals(CredentialFormat.VC_SD_JWT, theCredential.getCredentialFormat()); + if (theCredential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(3, sdJwtCredential.getDisclosures().size()); + } else { + fail("It should be an SD-JWT credential."); + } + } + + @Test + @DisplayName("sdJwtVc with 'multiple' set to true succeeds") + void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); + + assertEquals(2, credentialsResult.size()); + } + + @Test + @DisplayName("sdJwtVc with 'multiple' set to true but only one credential in the presentation matches") + void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + + assertEquals(1, credentialsResult.size()); + assertEquals(CredentialFormat.VC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(3, sdJwtCredential.getDisclosures().size()); + } else { + fail("An SdJwtCredential should be contained."); + } + } + + @Test + @DisplayName("sdJwtVc with no claims should not disclose anything.") + void sdJwtVcWithNoClaims() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + + assertEquals(1, credentialsResult.size()); + assertEquals(CredentialFormat.VC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); + if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(sdJwtCredential.getDisclosures().isEmpty()); + } else { + fail("An SdJwtCredential should be contained."); + } + } + +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java new file mode 100644 index 0000000..f0d6509 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java @@ -0,0 +1,139 @@ + +package io.github.wistefan.dcql.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.DcqlQuery; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.MDocHeaders; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DcqlQueryTrustedAuthoritiesTest extends DcqlTest { + + + // --- Test Data --- + + private static final Map ETSI_TL_AUTHORITY = Map.of("type", "etsi_tl", "values", List.of("https://list.com")); + private static final Map OPENID_FEDERATION_AUTHORITY = Map.of("type", "openid_federation", "values", List.of("https://federation.com")); + + + private static final String MDOC_MVRC_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"mso_mdoc\",\n" + + " \"trusted_authorities\": [\n" + + " {\n" + + " \"type\": \"aki\",\n" + + " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\", \"UVVJUkVELiBBIHN0cmluZyB1bmlxdWVseSBpZGVudGlmeWluZyB0aGUgdHlwZSA\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_ALT_AKI = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(generateTestKeyPair()))), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_NO_X5C = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of()), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final String SD_JWT_VC_EXAMPLE_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"vc+sd-jwt\",\n" + + " \"trusted_authorities\": [\n" + + " {\n" + + " \"type\": \"aki\",\n" + + " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final Credential SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential(new JwtCredential(Map.of("x5c", List.of(generateTestCertificate(TEST_KEY))), Map.of( + "credential_format", "vc+sd-jwt", + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of("first_name", "Arthur", "last_name", "Dent"), + "cryptographic_holder_binding", true + ), null), List.of())); + + + @Test + @DisplayName("mdocMvrc example with trusted_authorities succeeds") + void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertEquals(1, credentialsResult.size()); + } + + @Test + @DisplayName("mdocMvrc example where authority does not match trusted_authorities entry") + void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); + + assertEquals(0, credentialsResult.size()); + } + + @Test + @DisplayName("mdocMvrc example where trusted_authorities is present but no authority") + void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); + + assertEquals(0, credentialsResult.size()); + } + + @Test + @DisplayName("sdJwtVc example with trusted_authorities succeeds") + void sdJwtVcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); + + assertEquals(1, credentialsResult.size()); + } + +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java new file mode 100644 index 0000000..0120e71 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java @@ -0,0 +1,127 @@ + +package io.github.wistefan.dcql.query; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.DcqlQuery; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class DcqlQueryWithJsonTransformTest extends DcqlTest { + + /** + * A placeholder for a class that might be used in credential data and needs special handling, + * similar to the ValueClass in the TypeScript test. + */ + static class ValueClass { + private final Object value; + + public ValueClass(Object value) { + this.value = value; + } + + public Object toJson() { + return this.value; + } + + @Override + public boolean equals(Object o) { + return o instanceof ValueClass && ((ValueClass) o).value.equals(this.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + // --- Test Data --- + + private static final String MDOC_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ] + } + ] + } + """; + + private static final String SD_JWT_VC_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "dc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ + { "path": ["last_name"] }, + { "path": ["first_name"] }, + { "path": ["address", "street_address"] }, + { "path": ["org.iso.7367.1", "vehicle_holder"], "values": ["Timo Glastra"] } + ] + } + ] + } + """; + + private static final Credential MDOC_WITH_JT = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", new ValueClass("Martin Auer")) + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential SD_JWT_VC_WITH_JT = new Credential(CredentialFormat.DC_SD_JWT, + new SdJwtCredential( + new JwtCredential(null, Map.of( + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of( + "first_name", "Arthur", + "last_name", "Dent", + "address", Map.of("street_address", new ValueClass("42 Market Street"), "locality", "Milliways", "postal_code", "12345"), + "org.iso.7367.1", Map.of("vehicle_holder", "Timo Glastra") + ), + "cryptographic_holder_binding", true + ), null), List.of())); + + @Test + @DisplayName("mdocMvrc example succeeds") + void mdocMvrcExampleSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); + assertEquals(1, credentialsResult.size()); + } + + @Test + @DisplayName("sdJwtVc example with multiple credentials succeeds") + void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); + + assertEquals(1, credentialsResult.size()); + assertEquals(CredentialFormat.DC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); + } + +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java new file mode 100644 index 0000000..91ee178 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java @@ -0,0 +1,105 @@ +package io.github.wistefan.dcql.query; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.cert.X509ExtensionUtils; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Date; + +public abstract class DcqlTest { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + { + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public static final KeyPair TEST_KEY = generateTestKeyPair(); + + + public static KeyPair generateTestKeyPair() { + try { + // Generate keypair + KeyPairGenerator keyGen = null; + + keyGen = KeyPairGenerator.getInstance("RSA"); + + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static AuthorityKeyIdentifier generateTestAki(KeyPair testKey) { + + Security.addProvider(new BouncyCastleProvider()); + try { + X509ExtensionUtils extUtils = new X509ExtensionUtils( + new JcaDigestCalculatorProviderBuilder() + .setProvider("BC") + .build() + .get(new AlgorithmIdentifier(X509ObjectIdentifiers.id_SHA1)) + ); + + // Now you can create SKI and AKI + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(testKey.getPublic().getEncoded()); + + return extUtils.createAuthorityKeyIdentifier(subjectPublicKeyInfo); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + + public static X509Certificate generateTestCertificate(KeyPair testKey) { + + Security.addProvider(new BouncyCastleProvider()); + try { + Security.addProvider(new BouncyCastleProvider()); + // Certificate details + String issuer = "CN=Test CA"; + String subject = "CN=Test Cert"; + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60); + Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365)); + + // Builder + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + new javax.security.auth.x500.X500Principal(issuer), + serial, + notBefore, + notAfter, + new javax.security.auth.x500.X500Principal(subject), + testKey.getPublic() + ); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, generateTestAki(testKey)); + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .build(testKey.getPrivate()); + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certBuilder.build(signer)); + return cert; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} From 3eef529197b4da362d380ba759ddcc649343b7b7 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 12 Sep 2025 11:15:38 +0200 Subject: [PATCH 2/8] support multiple, remove jackson --- pom.xml | 23 ++++++------- .../github/wistefan/dcql/DCQLEvaluator.java | 5 +++ .../wistefan/dcql/model/ClaimsQuery.java | 4 --- .../wistefan/dcql/model/CredentialFormat.java | 4 --- .../wistefan/dcql/model/CredentialQuery.java | 6 +--- .../github/wistefan/dcql/model/DcqlQuery.java | 2 -- .../dcql/model/TrustedAuthorityType.java | 6 +--- .../dcql/result/ClaimEvaluationResult.java | 17 ---------- .../wistefan/dcql/result/ClaimSetResult.java | 18 ---------- .../wistefan/dcql/result/ClaimsResult.java | 19 ----------- .../result/CredentialEvaluationResult.java | 14 -------- .../wistefan/dcql/result/CredentialMatch.java | 14 -------- .../dcql/result/CredentialSetResult.java | 14 -------- .../wistefan/dcql/result/MetaResult.java | 14 -------- .../wistefan/dcql/result/QueryResult.java | 17 ---------- .../dcql/result/TrustedAuthoritiesResult.java | 15 --------- .../TrustedAuthorityEvaluationResult.java | 16 --------- .../helper/CredentialFormatDeserializer.java | 22 +++++++++++++ .../TrustedAuthorityTypeDeserializer.java | 22 +++++++++++++ .../dcql/query/DcqlClaimSetQueryTest.java | 33 +++++++++++++++++++ .../dcql/query/DcqlQueryComplexTest.java | 8 ++--- .../wistefan/dcql/query/DcqlQueryTest.java | 1 - .../github/wistefan/dcql/query/DcqlTest.java | 11 +++++++ 23 files changed, 109 insertions(+), 196 deletions(-) delete mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/MetaResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/QueryResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java delete mode 100644 src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java create mode 100644 src/test/java/io/github/wistefan/dcql/helper/CredentialFormatDeserializer.java create mode 100644 src/test/java/io/github/wistefan/dcql/helper/TrustedAuthorityTypeDeserializer.java diff --git a/pom.xml b/pom.xml index 992324b..1e7da37 100644 --- a/pom.xml +++ b/pom.xml @@ -91,17 +91,6 @@ - - com.fasterxml.jackson.core - jackson-databind - ${version.com.fasterxml.jackson.core} - provided - - - com.fasterxml.jackson.core - jackson-core - ${version.com.fasterxml.jackson.core} - org.projectlombok lombok @@ -129,6 +118,18 @@ 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 diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java index cfe0909..62d99a3 100644 --- a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -21,8 +21,13 @@ public static List evaluateDCQLQuery(DcqlQuery dcqlQuery, List path, List values) { /** * Identifier for the data-element in the namespace */ - @JsonProperty("claim_name") private String claimName; } diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java index 5e7f7ef..4578696 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java @@ -1,7 +1,5 @@ package io.github.wistefan.dcql.model; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import java.util.Arrays; @@ -21,7 +19,6 @@ public enum CredentialFormat { this.value = value; } - @JsonCreator public static CredentialFormat fromValue(String value) { return Arrays.stream(values()) .filter(eV -> eV.getValue().equals(value)) @@ -29,7 +26,6 @@ public static CredentialFormat fromValue(String value) { .orElseThrow(() -> new IllegalArgumentException(String.format("Unknown value %s.", value))); } - @JsonValue public String getValue() { return value; } diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java index 5ad1774..8948891 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java @@ -1,7 +1,6 @@ package io.github.wistefan.dcql.model; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -27,7 +26,7 @@ public class CredentialQuery { /** * A boolean which indicates whether multiple Credentials can be returned for this Credential Query. If omitted, the - * default value is false. If empty, no specific constraints are placed on the metadata or validity of the requested Credential. + * default value is false. */ private Boolean multiple = false; @@ -49,14 +48,12 @@ public class CredentialQuery { * is true, i.e., a Verifiable Presentation with Cryptographic Holder Binding is required. If set to false, the * Verifier accepts a Credential without Cryptographic Holder Binding proof. */ - @JsonProperty("require_cryptographic_holder_binding") private Boolean requireCryptographicHolderBinding; /** * A non-empty array containing arrays of identifiers for elements in claims that specifies which combinations of * claims for the Credential are requested. */ - @JsonProperty("claim_sets") private List> claimSets; /** @@ -64,6 +61,5 @@ public class CredentialQuery { * the Verifier will accept. Every Credential returned by the Wallet SHOULD match at least one of the conditions * present in the corresponding trusted_authorities array if present. */ - @JsonProperty("trusted_authorities") private List trustedAuthorities; } diff --git a/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java index e0cad7b..2ed765c 100644 --- a/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java @@ -1,7 +1,6 @@ package io.github.wistefan.dcql.model; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @@ -21,7 +20,6 @@ public class DcqlQuery { * A non-empty array of Credential Set Queries that specifies additional constraints on which of the requested * Credentials to return. */ - @JsonProperty("credential_sets") private List credentialSets; } diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java index 7c28424..57285b6 100644 --- a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java @@ -1,13 +1,11 @@ package io.github.wistefan.dcql.model; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; import lombok.Getter; import java.util.Arrays; public enum TrustedAuthorityType { - + AKI("aki"), ETSI_TL("etsi_tl"), OPENID_FEDERATION("openid_federation"); @@ -19,7 +17,6 @@ public enum TrustedAuthorityType { this.value = value; } - @JsonCreator public static TrustedAuthorityType fromValue(String value) { return Arrays.stream(values()) .filter(eV -> eV.getValue().equals(value)) @@ -27,7 +24,6 @@ public static TrustedAuthorityType fromValue(String value) { .orElseThrow(() -> new IllegalArgumentException(String.format("Unknown value %s.", value))); } - @JsonValue public String getValue() { return value; } diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java deleted file mode 100644 index b7aa59d..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/ClaimEvaluationResult.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ClaimEvaluationResult( - boolean success, - @JsonProperty("claim_index") int claimIndex, - @JsonProperty("claim_id") String claimId, - Map output, - Map> issues -) { -} diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java deleted file mode 100644 index e286fdd..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/ClaimSetResult.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ClaimSetResult( - boolean success, - @JsonProperty("claim_set_index") Integer claimSetIndex, - Map output, - @JsonProperty("valid_claim_indexes") List validClaimIndexes, - @JsonProperty("failed_claim_indexes") List failedClaimIndexes, - Map> issues -) { -} \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java b/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java deleted file mode 100644 index 81c2cf4..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/ClaimsResult.java +++ /dev/null @@ -1,19 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record ClaimsResult( - boolean success, - @JsonProperty("valid_claim_sets") List validClaimSets, - @JsonProperty("failed_claim_sets") List failedClaimSets, - @JsonProperty("valid_claims") List validClaims, - @JsonProperty("failed_claims") List failedClaims -) {} - - - diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java deleted file mode 100644 index bcf4aa0..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/CredentialEvaluationResult.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record CredentialEvaluationResult( - boolean success, - @JsonProperty("input_credential_index") int inputCredentialIndex, - MetaResult meta, - ClaimsResult claims, - @JsonProperty("trusted_authorities") TrustedAuthoritiesResult trustedAuthorities -) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java b/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java deleted file mode 100644 index 3621148..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/CredentialMatch.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record CredentialMatch( - boolean success, - @JsonProperty("credential_query_id") String credentialQueryId, - @JsonProperty("valid_credentials") List validCredentials, - @JsonProperty("failed_credentials") List failedCredentials -) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java b/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java deleted file mode 100644 index 3b65c4d..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/CredentialSetResult.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record CredentialSetResult( - String purpose, - Boolean required, - List> options, - @JsonProperty("matching_options") List> matchingOptions -) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/MetaResult.java b/src/main/java/io/github/wistefan/dcql/result/MetaResult.java deleted file mode 100644 index ff199fc..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/MetaResult.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record MetaResult( - boolean success, - Map output, - Map> issues -) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/QueryResult.java b/src/main/java/io/github/wistefan/dcql/result/QueryResult.java deleted file mode 100644 index 68fb4cd..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/QueryResult.java +++ /dev/null @@ -1,17 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -// Using @JsonInclude(JsonInclude.Include.NON_NULL) to omit null fields from JSON output, matching TS behavior - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record QueryResult( - boolean canBeSatisfied, - @JsonProperty("credential_matches") Map credentialMatches, - @JsonProperty("credential_sets") List credentialSets -) {} diff --git a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java deleted file mode 100644 index 021ac8f..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthoritiesResult.java +++ /dev/null @@ -1,15 +0,0 @@ - -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record TrustedAuthoritiesResult( - boolean success, - @JsonProperty("valid_trusted_authority") TrustedAuthorityEvaluationResult validTrustedAuthority, - @JsonProperty("failed_trusted_authorities") List failedTrustedAuthorities -) {} - diff --git a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java b/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java deleted file mode 100644 index d73efd9..0000000 --- a/src/main/java/io/github/wistefan/dcql/result/TrustedAuthorityEvaluationResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.wistefan.dcql.result; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.List; -import java.util.Map; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public record TrustedAuthorityEvaluationResult( - boolean success, - @JsonProperty("trusted_authority_index") int trustedAuthorityIndex, - Map output, - Map> issues -) { -} diff --git a/src/test/java/io/github/wistefan/dcql/helper/CredentialFormatDeserializer.java b/src/test/java/io/github/wistefan/dcql/helper/CredentialFormatDeserializer.java new file mode 100644 index 0000000..3fe7577 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/helper/CredentialFormatDeserializer.java @@ -0,0 +1,22 @@ +package io.github.wistefan.dcql.helper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import io.github.wistefan.dcql.model.CredentialFormat; + +import java.io.IOException; + +public class CredentialFormatDeserializer extends StdDeserializer { + + public CredentialFormatDeserializer() { + super(CredentialFormat.class); + } + + @Override + public CredentialFormat deserialize(JsonParser jsonParser, DeserializationContext context) + throws IOException { + String value = jsonParser.getText(); + return CredentialFormat.fromValue(value); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/wistefan/dcql/helper/TrustedAuthorityTypeDeserializer.java b/src/test/java/io/github/wistefan/dcql/helper/TrustedAuthorityTypeDeserializer.java new file mode 100644 index 0000000..36e8f8c --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/helper/TrustedAuthorityTypeDeserializer.java @@ -0,0 +1,22 @@ +package io.github.wistefan.dcql.helper; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import io.github.wistefan.dcql.model.TrustedAuthorityType; + +import java.io.IOException; + +public class TrustedAuthorityTypeDeserializer extends StdDeserializer { + + public TrustedAuthorityTypeDeserializer() { + super(TrustedAuthorityType.class); + } + + @Override + public TrustedAuthorityType deserialize(JsonParser jsonParser, DeserializationContext context) + throws IOException { + String value = jsonParser.getText(); + return TrustedAuthorityType.fromValue(value); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java index 169e5f8..3f1319f 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java @@ -22,6 +22,30 @@ public class DcqlClaimSetQueryTest extends DcqlTest { { "id": "my_credential", "format": "mso_mdoc", + "multiple": true, + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, + { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "claim_sets": [ + ["b","c"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String MDOC_MVRC_QUERY_SINGLE = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "multiple": false, "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, "claims": [ { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, @@ -242,4 +266,13 @@ void mdocMvrcQueryOnlyOne() throws JsonProcessingException { assertTrue(credentialsResult.contains(MDOC_MVRC_NAME)); assertTrue(credentialsResult.contains(MDOC_MVRC_FULL)); } + + @Test + @DisplayName("mdoc mvrc query fails when multiple credentials match, but multiple is not allowed.") + void mdocMvrcQueryFailedMultiple() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY_SINGLE, DcqlQuery.class); + List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + + assertEquals(0, credentialsResult.size()); + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java index eaa1a00..48559cd 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java @@ -2,8 +2,6 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; @@ -11,16 +9,14 @@ import io.github.wistefan.dcql.model.credential.JwtCredential; import io.github.wistefan.dcql.model.credential.MDocCredential; import io.github.wistefan.dcql.model.credential.SdJwtCredential; -import io.github.wistefan.dcql.result.CredentialSetResult; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.Collections; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class DcqlQueryComplexTest extends DcqlTest { diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java index 38bf969..d396ac4 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java @@ -223,7 +223,6 @@ void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); - assertEquals(2, credentialsResult.size()); } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java index 91ee178..c0fbc66 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java @@ -2,6 +2,12 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.github.wistefan.dcql.helper.CredentialFormatDeserializer; +import io.github.wistefan.dcql.helper.TrustedAuthorityTypeDeserializer; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.TrustedAuthorityType; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.cert.X509ExtensionUtils; import org.bouncycastle.cert.X509v3CertificateBuilder; @@ -26,6 +32,11 @@ public abstract class DcqlTest { { OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + SimpleModule deserializerModule = new SimpleModule(); + deserializerModule.addDeserializer(CredentialFormat.class, new CredentialFormatDeserializer()); + deserializerModule.addDeserializer(TrustedAuthorityType.class, new TrustedAuthorityTypeDeserializer()); + OBJECT_MAPPER.registerModule(deserializerModule); } public static final KeyPair TEST_KEY = generateTestKeyPair(); From 8a9e65b0a350073ea7c03212c689696734dd609d Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 12 Sep 2025 11:22:06 +0200 Subject: [PATCH 3/8] remove jacoco --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e60bac9..0b7bfc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,8 +69,7 @@ jobs: - name: Run tests run: | - mvn clean test jacoco:report - #coveralls:report -Dcoveralls.token=${{ secrets.COVERALLS_TOKEN }} + mvn clean test - name: Build and release it env: From c582c89f5f5fd70e0b9610d60f1dbb03f0694255 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 12 Sep 2025 12:48:02 +0200 Subject: [PATCH 4/8] handle credential sets --- .../github/wistefan/dcql/DCQLEvaluator.java | 87 ++++++++++++++----- .../io/github/wistefan/dcql/QueryResult.java | 17 ++++ .../dcql/model/CredentialSetQuery.java | 10 ++- .../dcql/query/DcqlClaimSetQueryTest.java | 64 ++++++++------ .../dcql/query/DcqlQueryComplexTest.java | 68 ++++++++++++--- .../wistefan/dcql/query/DcqlQueryTest.java | 60 +++++++------ .../DcqlQueryTrustedAuthoritiesTest.java | 21 +++-- .../query/DcqlQueryWithJsonTransformTest.java | 14 +-- 8 files changed, 242 insertions(+), 99 deletions(-) create mode 100644 src/main/java/io/github/wistefan/dcql/QueryResult.java diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java index 62d99a3..cdd5e35 100644 --- a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -11,24 +11,65 @@ @Slf4j public class DCQLEvaluator { + private static final String DEFAULT_KEY = "credentials"; private static final String MDOC_NAMESPACE_KEY = "namespaces"; - public static List evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { - List selectedCredentials = new ArrayList<>(); - for (CredentialQuery cq : dcqlQuery.getCredentials()) { - List credentialsFullfilling = evaluateCredentialQuery(cq, credentialsList); - if (credentialsFullfilling.isEmpty()) { - log.debug("When one of the credentials requirements is not fulfilled, the query should fail."); - return List.of(); + + public static QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { + if (containsCredentialSets(dcqlQuery)) { + // linked map to contain set order + Map> resultMap = new LinkedHashMap<>(); + validateIds(dcqlQuery.getCredentials()); + Map credentialQueryMap = new HashMap<>(); + dcqlQuery.getCredentials() + .forEach(cq -> credentialQueryMap.put(cq.getId(), cq)); + for (CredentialSetQuery credentialSetQuery : dcqlQuery.getCredentialSets()) { + List credentialsForSet = evaluateCredentialSetQuery(credentialQueryMap, credentialSetQuery, credentialsList); + if (credentialsForSet.isEmpty() && credentialSetQuery.getRequired()) { + log.debug("The query cannot be fulfilled, since a required set is empty."); + return new QueryResult(false, Map.of()); + } + resultMap.put(purposeOrRandom(credentialSetQuery), credentialsForSet); } - if (!cq.getMultiple() && credentialsFullfilling.size() != 1) { - log.debug("Multiple credentials where returend for a query not allowing multiple."); - return List.of(); + return new QueryResult(true, resultMap); + } else { + List selectedCredentials = new ArrayList<>(); + for (CredentialQuery cq : dcqlQuery.getCredentials()) { + List credentialsFullfilling = evaluateCredentialQuery(cq, credentialsList); + if (credentialsFullfilling.isEmpty()) { + log.debug("When one of the credentials requirements is not fulfilled, the query should fail."); + return new QueryResult(false, Map.of()); + } + if (!cq.getMultiple() && credentialsFullfilling.size() != 1) { + log.debug("Multiple credentials where returend for a query not allowing multiple."); + return new QueryResult(false, Map.of()); + } + selectedCredentials.addAll(credentialsFullfilling); } - selectedCredentials.addAll(credentialsFullfilling); + // if no sets are requested, put the credentials at one + return new QueryResult(true, Map.of("credentials", selectedCredentials)); } - return selectedCredentials; + } + + private static List evaluateCredentialSetQuery(Map credentialQueryMap, + CredentialSetQuery credentialSetQuery, + List credentials) { + for (List option : credentialSetQuery.getOptions()) { + // set to prevent duplicates + Set fullfillingCredentials = new HashSet<>(); + fullfillingCredentials.addAll( + option.stream() + .map(credentialQueryMap::get) + .map(cq -> evaluateCredentialQuery(cq, credentials)) + .flatMap(List::stream) + .collect(Collectors.toSet())); + // return the first option that fulfills the query + if (!fullfillingCredentials.isEmpty()) { + return new ArrayList<>(fullfillingCredentials); + } + } + return List.of(); } private static List evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { @@ -198,14 +239,6 @@ private static List evaluateSdJwtCredentialsQuery(CredentialQue return disclosedCredentials; } - private static List evaluateSdJwtCredentialsClaimQuery(ClaimsQuery cq, List sdJwtCredentials) { - return sdJwtCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { return ldpCredentials.stream() .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) @@ -304,4 +337,18 @@ private static boolean containsMeta(CredentialQuery credentialQuery) { private static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { return credentialQuery.getTrustedAuthorities() != null && !credentialQuery.getTrustedAuthorities().isEmpty(); } + + private static boolean containsCredentialSets(DcqlQuery dcqlQuery) { + return dcqlQuery.getCredentialSets() != null && !dcqlQuery.getCredentialSets().isEmpty(); + } + + private static void validateIds(List credentialQueries) { + if (credentialQueries.stream().anyMatch(cq -> cq.getId() == null)) { + throw new IllegalArgumentException("All credentialQueries need to contain an id."); + } + } + + private static Object purposeOrRandom(CredentialSetQuery credentialSetQuery) { + return Optional.ofNullable(credentialSetQuery.getPurpose()).orElse(UUID.randomUUID().toString()); + } } diff --git a/src/main/java/io/github/wistefan/dcql/QueryResult.java b/src/main/java/io/github/wistefan/dcql/QueryResult.java new file mode 100644 index 0000000..4f4ca47 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/QueryResult.java @@ -0,0 +1,17 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.Credential; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Result of a DCQL evaluation + * + * @param success - did the query succeed + * @param credentials - the credentials returned by the query. If credential_sets is present, they are keyed by their + * purpose or if omitted a random id. + */ +public record QueryResult(boolean success, Map> credentials) { +} diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java index cdedc9c..f27eb0f 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java @@ -10,7 +10,7 @@ * case with the Verifier. */ @Data -public class CredentialSetQuery{ +public class CredentialSetQuery { /** * A non-empty array, where each value in the array is a list of Credential Query identifiers representing one set @@ -24,4 +24,12 @@ public class CredentialSetQuery{ * Verifier. */ private Boolean required = true; + + /** + * A string, number or object specifying the purpose of the query. This specification does not define a specific + * structure or specific values for this property. The purpose is intended to be used by the Verifier to communicate + * the reason for the query to the Wallet. The Wallet MAY use this information to show the user the reason for the + * request. + */ + private Object purpose; } \ No newline at end of file diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java index 3f1319f..895683c 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; @@ -182,10 +183,12 @@ public class DcqlClaimSetQueryTest extends DcqlTest { @DisplayName("sd-jwt query get alternative") void sdJwtQueryGetAlternative() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); - assertEquals(1, credentialsResult.size()); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(1, sdJwtCredential.getDisclosures().size()); } else { fail("Did not get an SdJwt Credential."); @@ -197,10 +200,12 @@ void sdJwtQueryGetAlternative() throws JsonProcessingException { @DisplayName("sd-jwt query get for name") void sdJwtQueryForName() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); - assertEquals(1, credentialsResult.size()); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(2, sdJwtCredential.getDisclosures().size()); } else { fail("Did not get an SdJwt Credential."); @@ -211,10 +216,12 @@ void sdJwtQueryForName() throws JsonProcessingException { @DisplayName("sd-jwt query get for street_address within full") void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); - assertEquals(1, credentialsResult.size()); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(1, sdJwtCredential.getDisclosures().size()); } else { fail("Did not get an SdJwt Credential."); @@ -225,11 +232,13 @@ void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { @DisplayName("sd-jwt query get for street_address") void sdJwtQueryForStreetAddress() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); - assertEquals(1, credentialsResult.size()); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(1, sdJwtCredential.getDisclosures().size()); } else { fail("Did not get an SdJwt Credential."); @@ -240,39 +249,44 @@ void sdJwtQueryForStreetAddress() throws JsonProcessingException { @DisplayName("mdoc mvrc query get full doc") void mdocMvrcQueryFullDocSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); - - assertEquals(1, credentialsResult.size()); - assertEquals(credentialsResult.get(0), MDOC_MVRC_FULL); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(credential, MDOC_MVRC_FULL); } @Test @DisplayName("mdoc mvrc query get second set") void mdocMvrcQuerySecondSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); - assertEquals(1, credentialsResult.size()); - assertEquals(credentialsResult.get(0), MDOC_MVRC_HOLDER); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(credential, MDOC_MVRC_HOLDER); } @Test @DisplayName("mdoc mvrc query gets the fullfilling credentials.") void mdocMvrcQueryOnlyOne() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); - assertEquals(2, credentialsResult.size()); - assertTrue(credentialsResult.contains(MDOC_MVRC_NAME)); - assertTrue(credentialsResult.contains(MDOC_MVRC_FULL)); + assertTrue(queryResult.success()); + assertEquals(2, queryResult.credentials().get("credentials").size()); + List credentials = queryResult.credentials().get("credentials"); + assertTrue(credentials.contains(MDOC_MVRC_NAME)); + assertTrue(credentials.contains(MDOC_MVRC_FULL)); } @Test @DisplayName("mdoc mvrc query fails when multiple credentials match, but multiple is not allowed.") void mdocMvrcQueryFailedMultiple() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY_SINGLE, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); - assertEquals(0, credentialsResult.size()); + assertFalse(queryResult.success()); } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java index 48559cd..cc3da83 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; @@ -15,8 +16,7 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class DcqlQueryComplexTest extends DcqlTest { @@ -129,9 +129,9 @@ class DcqlQueryComplexTest extends DcqlTest { void failsWithNoCredentials() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); - assertEquals(0, credentialsResult.size()); + assertFalse(queryResult.success()); } @Test @@ -139,18 +139,19 @@ void failsWithNoCredentials() throws JsonProcessingException { void failsWithCredentialsThatDoNotSatisfyARequiredClaimSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); - assertEquals(0, credentialsResult.size()); + assertFalse(queryResult.success()); } @Test - @DisplayName("succeeds if all credentials are present") - void succeedsIfAllCredentialsArePresent() throws JsonProcessingException { - List expectedCredentials = List.of(MDOC_MDL_ID, MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ID, MDOC_PHOTO_CARD_ADDRESS); + @DisplayName("return the requested sets") + void succeedsWithRequestedSets() throws JsonProcessingException { + List expectedIdCredentials = List.of(MDOC_MDL_ID); + List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( MDOC_MDL_ID, MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ID, @@ -158,11 +159,50 @@ void succeedsIfAllCredentialsArePresent() throws JsonProcessingException { MDOC_EXAMPLE, SD_JWT_VC_EXAMPLE)); - assertEquals(4, credentialsResult.size(), "Only the MDoc Credentials of th reight type should be included."); + assertTrue(queryResult.success()); + assertTrue(queryResult.credentials().containsKey("Identification")); + assertTrue(queryResult.credentials().containsKey("Proof of address")); - expectedCredentials.forEach( - ec -> assertTrue(credentialsResult.contains(ec)) - ); + List identification = queryResult.credentials().get("Identification"); + List poa = queryResult.credentials().get("Proof of address"); + + assertEquals(1, identification.size()); + assertEquals(1, poa.size()); + + expectedIdCredentials.forEach( + ec -> assertTrue(identification.contains(ec))); + expectedPoaCredentials.forEach( + ec -> assertTrue(poa.contains(ec))); + } + + @Test + @DisplayName("return alternative if not included") + void returnAlternative() throws JsonProcessingException { + List expectedIdCredentials = List.of(MDOC_PHOTO_CARD_ID); + List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + MDOC_MDL_ADDRESS, + MDOC_PHOTO_CARD_ID, + MDOC_PHOTO_CARD_ADDRESS, + MDOC_EXAMPLE, + SD_JWT_VC_EXAMPLE)); + + assertTrue(queryResult.success()); + assertTrue(queryResult.credentials().containsKey("Identification")); + assertTrue(queryResult.credentials().containsKey("Proof of address")); + + List identification = queryResult.credentials().get("Identification"); + List poa = queryResult.credentials().get("Proof of address"); + + assertEquals(1, identification.size()); + assertEquals(1, poa.size()); + + expectedIdCredentials.forEach( + ec -> assertTrue(identification.contains(ec))); + expectedPoaCredentials.forEach( + ec -> assertTrue(poa.contains(ec))); } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java index d396ac4..507d662 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; @@ -157,46 +158,50 @@ class DcqlQueryTest extends DcqlTest { @DisplayName("mdoc mvrc query fails with invalid mdoc") void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); - assertEquals(0, credentialsResult.size()); + assertFalse(queryResult.success()); } @Test @DisplayName("mdoc mvrc example with multiple credentials succeeds") void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); - assertEquals(1, credentialsResult.size()); - assertEquals(MDOC_MVRC, credentialsResult.get(0)); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(MDOC_MVRC, queryResult.credentials().get("credentials").get(0)); } @Test @DisplayName("w3cLdpVc example succeeds") void w3cLdpVcExampleSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); - assertEquals(1, credentialsResult.size()); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(EXAMPLE_W3C_LDP_VC, queryResult.credentials().get("credentials").get(0)); } @Test @DisplayName("w3cLdpVc query fails with invalid type values") void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - assertTrue(credentialsResult.isEmpty()); + assertFalse(queryResult.success()); } @Test @DisplayName("mdocMvrc example using namespaces succeeds") void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_NAMESPACE_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - assertEquals(1, credentialsResult.size()); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); } @Test @@ -204,11 +209,11 @@ void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); - assertFalse(credentialsResult.isEmpty()); - assertEquals(1, credentialsResult.size()); - Credential theCredential = credentialsResult.get(0); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential theCredential = queryResult.credentials().get("credentials").get(0); assertEquals(CredentialFormat.VC_SD_JWT, theCredential.getCredentialFormat()); if (theCredential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(3, sdJwtCredential.getDisclosures().size()); @@ -222,8 +227,9 @@ void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingExcept void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); - assertEquals(2, credentialsResult.size()); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); + assertTrue(queryResult.success()); + assertEquals(2, queryResult.credentials().get("credentials").size()); } @Test @@ -231,11 +237,13 @@ void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); - assertEquals(1, credentialsResult.size()); - assertEquals(CredentialFormat.VC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertEquals(3, sdJwtCredential.getDisclosures().size()); } else { fail("An SdJwtCredential should be contained."); @@ -247,11 +255,13 @@ void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { void sdJwtVcWithNoClaims() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); - assertEquals(1, credentialsResult.size()); - assertEquals(CredentialFormat.VC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); - if (credentialsResult.get(0).getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { assertTrue(sdJwtCredential.getDisclosures().isEmpty()); } else { fail("An SdJwtCredential should be contained."); diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java index f0d6509..f8411a6 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; @@ -18,7 +19,7 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; class DcqlQueryTrustedAuthoritiesTest extends DcqlTest { @@ -101,9 +102,10 @@ class DcqlQueryTrustedAuthoritiesTest extends DcqlTest { @DisplayName("mdocMvrc example with trusted_authorities succeeds") void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - assertEquals(1, credentialsResult.size()); + assertTrue(credentialsResult.success()); + assertEquals(1, credentialsResult.credentials().get("credentials").size()); } @Test @@ -111,9 +113,9 @@ void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingExcept void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); - assertEquals(0, credentialsResult.size()); + assertFalse(credentialsResult.success()); } @Test @@ -121,9 +123,9 @@ void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); - assertEquals(0, credentialsResult.size()); + assertFalse(credentialsResult.success()); } @Test @@ -131,9 +133,10 @@ void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { void sdJwtVcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); - assertEquals(1, credentialsResult.size()); + assertTrue(credentialsResult.success()); + assertEquals(1, credentialsResult.credentials().get("credentials").size()); } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java index 0120e71..24c2080 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.github.wistefan.dcql.DCQLEvaluator; +import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; @@ -110,18 +111,21 @@ public int hashCode() { @DisplayName("mdocMvrc example succeeds") void mdocMvrcExampleSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); - assertEquals(1, credentialsResult.size()); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); } @Test @DisplayName("sdJwtVc example with multiple credentials succeeds") void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - List credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); - assertEquals(1, credentialsResult.size()); - assertEquals(CredentialFormat.DC_SD_JWT, credentialsResult.get(0).getCredentialFormat()); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(CredentialFormat.DC_SD_JWT, queryResult.credentials().get("credentials").get(0).getCredentialFormat()); } } From 521484797236a41ac29214ccba545d74fb6c45e9 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 12 Sep 2025 13:56:30 +0200 Subject: [PATCH 5/8] Add constructors --- .../io/github/wistefan/dcql/model/TrustedAuthorityQuery.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java index c2a1284..9737710 100644 --- a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java @@ -1,7 +1,9 @@ package io.github.wistefan.dcql.model; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; @@ -10,6 +12,8 @@ * A Credential is identified as a match to a Trusted Authorities Query if it matches with one of the provided values in * one of the provided types. */ +@AllArgsConstructor +@NoArgsConstructor @Data public class TrustedAuthorityQuery { From d666918db0c60c415ff1afa66a9facc3f7417f4f Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Fri, 26 Sep 2025 13:39:16 +0200 Subject: [PATCH 6/8] review fixes --- .../github/wistefan/dcql/ClaimsEvaluator.java | 5 +- .../github/wistefan/dcql/DCQLEvaluator.java | 680 +++++++++--------- 2 files changed, 343 insertions(+), 342 deletions(-) diff --git a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java index 06787f0..2697d21 100644 --- a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java @@ -10,6 +10,7 @@ @Slf4j public class ClaimsEvaluator { + // key for selective disclosure values inside the VC private static final String SD_KEY = "_sd"; public static Optional evaluateClaimsForMDocCredential(ClaimsQuery claimsQuery, MDocCredential credential) { @@ -129,8 +130,8 @@ private static List processPath( Object candidate = candidateWrapper.value; // If map contains _sd, reveal it and MERGE revealed entries with the original map - if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey("_sd")) { - Object sdObj = mapCandidate.get("_sd"); + if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey(SD_KEY)) { + Object sdObj = mapCandidate.get(SD_KEY); Map revealed = getStringSelectedClaimMap(disclosures, sdObj); // Merge: start with revealed, then copy original entries (except "_sd"), diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java index cdd5e35..a41ba2f 100644 --- a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -11,344 +11,344 @@ @Slf4j public class DCQLEvaluator { - private static final String DEFAULT_KEY = "credentials"; - private static final String MDOC_NAMESPACE_KEY = "namespaces"; - - - public static QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { - if (containsCredentialSets(dcqlQuery)) { - // linked map to contain set order - Map> resultMap = new LinkedHashMap<>(); - validateIds(dcqlQuery.getCredentials()); - Map credentialQueryMap = new HashMap<>(); - dcqlQuery.getCredentials() - .forEach(cq -> credentialQueryMap.put(cq.getId(), cq)); - for (CredentialSetQuery credentialSetQuery : dcqlQuery.getCredentialSets()) { - List credentialsForSet = evaluateCredentialSetQuery(credentialQueryMap, credentialSetQuery, credentialsList); - if (credentialsForSet.isEmpty() && credentialSetQuery.getRequired()) { - log.debug("The query cannot be fulfilled, since a required set is empty."); - return new QueryResult(false, Map.of()); - } - resultMap.put(purposeOrRandom(credentialSetQuery), credentialsForSet); - } - return new QueryResult(true, resultMap); - } else { - List selectedCredentials = new ArrayList<>(); - for (CredentialQuery cq : dcqlQuery.getCredentials()) { - List credentialsFullfilling = evaluateCredentialQuery(cq, credentialsList); - if (credentialsFullfilling.isEmpty()) { - log.debug("When one of the credentials requirements is not fulfilled, the query should fail."); - return new QueryResult(false, Map.of()); - } - if (!cq.getMultiple() && credentialsFullfilling.size() != 1) { - log.debug("Multiple credentials where returend for a query not allowing multiple."); - return new QueryResult(false, Map.of()); - } - selectedCredentials.addAll(credentialsFullfilling); - } - // if no sets are requested, put the credentials at one - return new QueryResult(true, Map.of("credentials", selectedCredentials)); - } - - } - - private static List evaluateCredentialSetQuery(Map credentialQueryMap, - CredentialSetQuery credentialSetQuery, - List credentials) { - for (List option : credentialSetQuery.getOptions()) { - // set to prevent duplicates - Set fullfillingCredentials = new HashSet<>(); - fullfillingCredentials.addAll( - option.stream() - .map(credentialQueryMap::get) - .map(cq -> evaluateCredentialQuery(cq, credentials)) - .flatMap(List::stream) - .collect(Collectors.toSet())); - // return the first option that fulfills the query - if (!fullfillingCredentials.isEmpty()) { - return new ArrayList<>(fullfillingCredentials); - } - } - return List.of(); - } - - private static List evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { - - if (!containsClaims(credentialQuery) - && containsClaims(credentialQuery)) { - throw new IllegalArgumentException("Queries with claim_set require to have claims, too."); - } - - List filteredByFormat = filterByFormat(credentialQuery.getFormat(), credentialsList); - return switch (credentialQuery.getFormat()) { - case LDP_VC -> evaluateForLdpVC(credentialQuery, filteredByFormat); - case MSO_MDOC -> evaluateForMDoc(credentialQuery, filteredByFormat); - case DC_SD_JWT, VC_SD_JWT -> evaluateForSdJwt(credentialQuery, filteredByFormat); - case JWT_VC_JSON -> evaluateForJwt(credentialQuery, filteredByFormat); - }; - } - - private static List evaluateForSdJwt(CredentialQuery credentialQuery, List credentialsList) { - List sdJwtCredentials = CredentialMapper.toSdJWTCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - sdJwtCredentials = filterSdJwtByMetadata(credentialQuery.getMeta(), sdJwtCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - sdJwtCredentials = sdJwtCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForSDJwtCredential(taq, credential)) - .toList(); - } - } - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - sdJwtCredentials = evaluateSdJwtCredentialsQuery(credentialQuery, sdJwtCredentials); - } else if (containsClaims(credentialQuery)) { - return evaluateSdJwtForClaimSet(credentialQuery, sdJwtCredentials); - } else { - sdJwtCredentials = sdJwtCredentials.stream() - // keep the original credential untouched - .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getJwtCredential(), List.of())) - .toList(); - } - return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); - } - - private static List evaluateForJwt(CredentialQuery credentialQuery, List credentialsList) { - List jwtCredentials = CredentialMapper.toJWTCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - jwtCredentials = filterJwtByMetadata(credentialQuery.getMeta(), jwtCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - jwtCredentials = jwtCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForJwtCredential(taq, credential)) - .toList(); - } - } - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - jwtCredentials = evaluateJwtCredentialsClaimQuery(cq, jwtCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, jwtCredentials, DCQLEvaluator::evaluateJwtCredentialsClaimQuery); - } - return CredentialMapper.toCredentials(CredentialFormat.JWT_VC_JSON, jwtCredentials); - } - - private static List evaluateForLdpVC(CredentialQuery credentialQuery, List credentialsList) { - List ldpCredentials = CredentialMapper.toLdpCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - ldpCredentials = filterLdpByMetadata(credentialQuery.getMeta(), ldpCredentials); - } - - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - ldpCredentials = evaluateLdpCredentialsClaimQuery(cq, ldpCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, ldpCredentials, DCQLEvaluator::evaluateLdpCredentialsClaimQuery); - } - - return CredentialMapper.toCredentials(CredentialFormat.LDP_VC, ldpCredentials); - } - - private static List evaluateForMDoc(CredentialQuery credentialQuery, List credentialsList) { - List mDocCredentials = CredentialMapper.toMDocCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - mDocCredentials = filterMDocByMetadata(credentialQuery.getMeta(), mDocCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - mDocCredentials = mDocCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForMDocCredential(taq, credential)) - .toList(); - } - } - translateMDocQueries(credentialQuery); - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - mDocCredentials = evaluateMDocCredentialsClaimQuery(cq, mDocCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, mDocCredentials, DCQLEvaluator::evaluateMDocCredentialsClaimQuery); - } - return CredentialMapper.toCredentials(CredentialFormat.MSO_MDOC, mDocCredentials); - } - - private static List evaluateSdJwtForClaimSet(CredentialQuery credentialQuery, List sdJwtCredentials) { - Map claimsQueryMap = new HashMap<>(); - credentialQuery.getClaims() - .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); - - for (List claimSet : credentialQuery.getClaimSets()) { - List disclosedCredentials = new ArrayList<>(); - for (SdJwtCredential credential : sdJwtCredentials) { - Set disclosures = new HashSet<>(); - for (String claimId : claimSet) { - ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); - disclosures.addAll(new HashSet<>( - ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, credential) - .map(SdJwtCredential::getDisclosures) - .orElse(new ArrayList<>()))); - } - if (!disclosures.isEmpty()) { - disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(disclosures))); - } - } - - if (!disclosedCredentials.isEmpty()) { - return CredentialMapper.toCredentials(credentialQuery.getFormat(), disclosedCredentials); - } - } - return List.of(); - } - - // The method returns the first claim set that is fullfilled. It can contain multiple credentials, that would - // fulfill the set individually, leaving the choice of what to share to the upstream. - private static List evaluateForClaimSet(CredentialQuery credentialQuery, List initialCredentials, BiFunction, List> evaluationFunction) { - Map claimsQueryMap = new HashMap<>(); - credentialQuery.getClaims() - .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); - - for (List claimSet : credentialQuery.getClaimSets()) { - List credentialsForClaimSet = new ArrayList<>(initialCredentials); - for (String claimId : claimSet) { - ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); - credentialsForClaimSet = evaluationFunction.apply(claimsQuery, credentialsForClaimSet); - } - if (!credentialsForClaimSet.isEmpty()) { - return CredentialMapper.toCredentials(credentialQuery.getFormat(), credentialsForClaimSet); - } - } - return List.of(); - } - - private static List evaluateSdJwtCredentialsQuery(CredentialQuery credentialQuery, List sdJwtCredentials) { - List disclosedCredentials = new ArrayList<>(); - for (SdJwtCredential credential : sdJwtCredentials) { - Set selectedDisclosures = credentialQuery.getClaims() - .stream() - .map(cq -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .map(SdJwtCredential::getDisclosures) - .flatMap(List::stream) - .collect(Collectors.toSet()); - disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); - } - return disclosedCredentials; - } - - private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { - return ldpCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static List evaluateJwtCredentialsClaimQuery(ClaimsQuery cq, List jwtCredentials) { - return jwtCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForJwtCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static List evaluateMDocCredentialsClaimQuery(ClaimsQuery cq, List mDocCredentials) { - return mDocCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForMDocCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static boolean isMDocClaimsQuery(ClaimsQuery claimsQuery) { - if ((claimsQuery.getNamespace() != null && claimsQuery.getClaimName() == null) || (claimsQuery.getNamespace() == null && claimsQuery.getClaimName() != null)) { - throw new IllegalArgumentException("When a namespace or claim_name is set, the other parameter is mandatory."); - } - return claimsQuery.getIntent_to_retain() != null || claimsQuery.getNamespace() != null; - } - - private static CredentialQuery translateMDocQueries(CredentialQuery credentialQuery) { - if (credentialQuery.getClaims() == null) { - return credentialQuery; - } - credentialQuery.getClaims() - .forEach(cq -> { - if (isMDocClaimsQuery(cq) && cq.getNamespace() != null) { - cq.setPath(List.of(MDOC_NAMESPACE_KEY, cq.getNamespace(), cq.getClaimName())); - } else { - cq.getPath().addFirst(MDOC_NAMESPACE_KEY); - } - }); - return credentialQuery; - } - - private static List filterByFormat(CredentialFormat credentialFormat, List credentialsList) { - return credentialsList.stream() - .filter(c -> c.getCredentialFormat() == credentialFormat) - .toList(); - } - - private static List filterLdpByMetadata(Map metaData, List credentialsList) { - W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(ldpCredential -> - w3CMetaData.getTypeValues() - .stream() - .anyMatch(metaTypes -> new HashSet<>(ldpCredential.getType()).containsAll(metaTypes))) - .toList(); - } - - private static List filterSdJwtByMetadata(Map metaData, List credentialsList) { - JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(sdJwtCredential -> jwtMetaData.getVctValues().contains(sdJwtCredential.getVct())) - .toList(); - } - - private static List filterJwtByMetadata(Map metaData, List credentialsList) { - JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(jwtCredential -> jwtMetaData.getVctValues().contains(jwtCredential.getVct())) - .toList(); - } - - private static List filterMDocByMetadata(Map metaData, List credentialsList) { - MDocMetaData mDocMetaData = MDocMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(mDocCredential -> mDocCredential.getDocType().equals(mDocMetaData.getDocType())) - .toList(); - } - - private static boolean containsClaims(CredentialQuery credentialQuery) { - return credentialQuery.getClaims() != null && !credentialQuery.getClaims().isEmpty(); - } - - private static boolean containsClaimSets(CredentialQuery credentialQuery) { - return credentialQuery.getClaimSets() != null && !credentialQuery.getClaimSets().isEmpty(); - } - - private static boolean containsMeta(CredentialQuery credentialQuery) { - return credentialQuery.getMeta() != null && !credentialQuery.getMeta().isEmpty(); - } - - private static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { - return credentialQuery.getTrustedAuthorities() != null && !credentialQuery.getTrustedAuthorities().isEmpty(); - } - - private static boolean containsCredentialSets(DcqlQuery dcqlQuery) { - return dcqlQuery.getCredentialSets() != null && !dcqlQuery.getCredentialSets().isEmpty(); - } - - private static void validateIds(List credentialQueries) { - if (credentialQueries.stream().anyMatch(cq -> cq.getId() == null)) { - throw new IllegalArgumentException("All credentialQueries need to contain an id."); - } - } - - private static Object purposeOrRandom(CredentialSetQuery credentialSetQuery) { - return Optional.ofNullable(credentialSetQuery.getPurpose()).orElse(UUID.randomUUID().toString()); - } + private static final String DEFAULT_KEY = "credentials"; + private static final String MDOC_NAMESPACE_KEY = "namespaces"; + + + public static QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { + if (containsCredentialSets(dcqlQuery)) { + // linked map to contain set order + Map> resultMap = new LinkedHashMap<>(); + validateIds(dcqlQuery.getCredentials()); + Map credentialQueryMap = new HashMap<>(); + dcqlQuery.getCredentials() + .forEach(cq -> credentialQueryMap.put(cq.getId(), cq)); + for (CredentialSetQuery credentialSetQuery : dcqlQuery.getCredentialSets()) { + List credentialsForSet = evaluateCredentialSetQuery(credentialQueryMap, credentialSetQuery, credentialsList); + if (credentialsForSet.isEmpty() && credentialSetQuery.getRequired()) { + log.debug("The query cannot be fulfilled, since a required set is empty."); + return new QueryResult(false, Map.of()); + } + resultMap.put(purposeOrRandom(credentialSetQuery), credentialsForSet); + } + return new QueryResult(true, resultMap); + } else { + List selectedCredentials = new ArrayList<>(); + for (CredentialQuery cq : dcqlQuery.getCredentials()) { + List credentialsFullfilling = evaluateCredentialQuery(cq, credentialsList); + if (credentialsFullfilling.isEmpty()) { + log.debug("When one of the credentials requirements is not fulfilled, the query should fail."); + return new QueryResult(false, Map.of()); + } + if (!cq.getMultiple() && credentialsFullfilling.size() != 1) { + log.debug("Multiple credentials where returend for a query not allowing multiple."); + return new QueryResult(false, Map.of()); + } + selectedCredentials.addAll(credentialsFullfilling); + } + // if no sets are requested, put the credentials at one + return new QueryResult(true, Map.of(DEFAULT_KEY, selectedCredentials)); + } + + } + + private static List evaluateCredentialSetQuery(Map credentialQueryMap, + CredentialSetQuery credentialSetQuery, + List credentials) { + for (List option : credentialSetQuery.getOptions()) { + // set to prevent duplicates + Set fullfillingCredentials = new HashSet<>(); + fullfillingCredentials.addAll( + option.stream() + .map(credentialQueryMap::get) + .map(cq -> evaluateCredentialQuery(cq, credentials)) + .flatMap(List::stream) + .collect(Collectors.toSet())); + // return the first option that fulfills the query + if (!fullfillingCredentials.isEmpty()) { + return new ArrayList<>(fullfillingCredentials); + } + } + return List.of(); + } + + private static List evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { + + if (!containsClaims(credentialQuery) + && containsClaimSets(credentialQuery)) { + throw new IllegalArgumentException("Queries with claim_set require to have claims, too."); + } + + List filteredByFormat = filterByFormat(credentialQuery.getFormat(), credentialsList); + return switch (credentialQuery.getFormat()) { + case LDP_VC -> evaluateForLdpVC(credentialQuery, filteredByFormat); + case MSO_MDOC -> evaluateForMDoc(credentialQuery, filteredByFormat); + case DC_SD_JWT, VC_SD_JWT -> evaluateForSdJwt(credentialQuery, filteredByFormat); + case JWT_VC_JSON -> evaluateForJwt(credentialQuery, filteredByFormat); + }; + } + + private static List evaluateForSdJwt(CredentialQuery credentialQuery, List credentialsList) { + List sdJwtCredentials = CredentialMapper.toSdJWTCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + sdJwtCredentials = filterSdJwtByMetadata(credentialQuery.getMeta(), sdJwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + sdJwtCredentials = sdJwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForSDJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + sdJwtCredentials = evaluateSdJwtCredentialsQuery(credentialQuery, sdJwtCredentials); + } else if (containsClaims(credentialQuery)) { + return evaluateSdJwtForClaimSet(credentialQuery, sdJwtCredentials); + } else { + sdJwtCredentials = sdJwtCredentials.stream() + // keep the original credential untouched + .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getJwtCredential(), List.of())) + .toList(); + } + return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); + } + + private static List evaluateForJwt(CredentialQuery credentialQuery, List credentialsList) { + List jwtCredentials = CredentialMapper.toJWTCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + jwtCredentials = filterJwtByMetadata(credentialQuery.getMeta(), jwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + jwtCredentials = jwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + jwtCredentials = evaluateJwtCredentialsClaimQuery(cq, jwtCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, jwtCredentials, DCQLEvaluator::evaluateJwtCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.JWT_VC_JSON, jwtCredentials); + } + + private static List evaluateForLdpVC(CredentialQuery credentialQuery, List credentialsList) { + List ldpCredentials = CredentialMapper.toLdpCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + ldpCredentials = filterLdpByMetadata(credentialQuery.getMeta(), ldpCredentials); + } + + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + ldpCredentials = evaluateLdpCredentialsClaimQuery(cq, ldpCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, ldpCredentials, DCQLEvaluator::evaluateLdpCredentialsClaimQuery); + } + + return CredentialMapper.toCredentials(CredentialFormat.LDP_VC, ldpCredentials); + } + + private static List evaluateForMDoc(CredentialQuery credentialQuery, List credentialsList) { + List mDocCredentials = CredentialMapper.toMDocCredentials(credentialsList); + if (containsMeta(credentialQuery)) { + mDocCredentials = filterMDocByMetadata(credentialQuery.getMeta(), mDocCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + mDocCredentials = mDocCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForMDocCredential(taq, credential)) + .toList(); + } + } + translateMDocQueries(credentialQuery); + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + mDocCredentials = evaluateMDocCredentialsClaimQuery(cq, mDocCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, mDocCredentials, DCQLEvaluator::evaluateMDocCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.MSO_MDOC, mDocCredentials); + } + + private static List evaluateSdJwtForClaimSet(CredentialQuery credentialQuery, List sdJwtCredentials) { + Map claimsQueryMap = new HashMap<>(); + credentialQuery.getClaims() + .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); + + for (List claimSet : credentialQuery.getClaimSets()) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set disclosures = new HashSet<>(); + for (String claimId : claimSet) { + ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); + disclosures.addAll(new HashSet<>( + ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, credential) + .map(SdJwtCredential::getDisclosures) + .orElse(new ArrayList<>()))); + } + if (!disclosures.isEmpty()) { + disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(disclosures))); + } + } + + if (!disclosedCredentials.isEmpty()) { + return CredentialMapper.toCredentials(credentialQuery.getFormat(), disclosedCredentials); + } + } + return List.of(); + } + + // The method returns the first claim set that is fullfilled. It can contain multiple credentials, that would + // fulfill the set individually, leaving the choice of what to share to the upstream. + private static List evaluateForClaimSet(CredentialQuery credentialQuery, List initialCredentials, BiFunction, List> evaluationFunction) { + Map claimsQueryMap = new HashMap<>(); + credentialQuery.getClaims() + .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); + + for (List claimSet : credentialQuery.getClaimSets()) { + List credentialsForClaimSet = new ArrayList<>(initialCredentials); + for (String claimId : claimSet) { + ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); + credentialsForClaimSet = evaluationFunction.apply(claimsQuery, credentialsForClaimSet); + } + if (!credentialsForClaimSet.isEmpty()) { + return CredentialMapper.toCredentials(credentialQuery.getFormat(), credentialsForClaimSet); + } + } + return List.of(); + } + + private static List evaluateSdJwtCredentialsQuery(CredentialQuery credentialQuery, List sdJwtCredentials) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set selectedDisclosures = credentialQuery.getClaims() + .stream() + .map(cq -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(SdJwtCredential::getDisclosures) + .flatMap(List::stream) + .collect(Collectors.toSet()); + disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); + } + return disclosedCredentials; + } + + private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { + return ldpCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static List evaluateJwtCredentialsClaimQuery(ClaimsQuery cq, List jwtCredentials) { + return jwtCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static List evaluateMDocCredentialsClaimQuery(ClaimsQuery cq, List mDocCredentials) { + return mDocCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForMDocCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static boolean isMDocClaimsQuery(ClaimsQuery claimsQuery) { + if ((claimsQuery.getNamespace() != null && claimsQuery.getClaimName() == null) || (claimsQuery.getNamespace() == null && claimsQuery.getClaimName() != null)) { + throw new IllegalArgumentException("When a namespace or claim_name is set, the other parameter is mandatory."); + } + return claimsQuery.getIntent_to_retain() != null || claimsQuery.getNamespace() != null; + } + + private static CredentialQuery translateMDocQueries(CredentialQuery credentialQuery) { + if (credentialQuery.getClaims() == null) { + return credentialQuery; + } + credentialQuery.getClaims() + .forEach(cq -> { + if (isMDocClaimsQuery(cq) && cq.getNamespace() != null) { + cq.setPath(List.of(MDOC_NAMESPACE_KEY, cq.getNamespace(), cq.getClaimName())); + } else { + cq.getPath().addFirst(MDOC_NAMESPACE_KEY); + } + }); + return credentialQuery; + } + + private static List filterByFormat(CredentialFormat credentialFormat, List credentialsList) { + return credentialsList.stream() + .filter(c -> c.getCredentialFormat() == credentialFormat) + .toList(); + } + + private static List filterLdpByMetadata(Map metaData, List credentialsList) { + W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(ldpCredential -> + w3CMetaData.getTypeValues() + .stream() + .anyMatch(metaTypes -> new HashSet<>(ldpCredential.getType()).containsAll(metaTypes))) + .toList(); + } + + private static List filterSdJwtByMetadata(Map metaData, List credentialsList) { + JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(sdJwtCredential -> jwtMetaData.getVctValues().contains(sdJwtCredential.getVct())) + .toList(); + } + + private static List filterJwtByMetadata(Map metaData, List credentialsList) { + JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(jwtCredential -> jwtMetaData.getVctValues().contains(jwtCredential.getVct())) + .toList(); + } + + private static List filterMDocByMetadata(Map metaData, List credentialsList) { + MDocMetaData mDocMetaData = MDocMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(mDocCredential -> mDocCredential.getDocType().equals(mDocMetaData.getDocType())) + .toList(); + } + + private static boolean containsClaims(CredentialQuery credentialQuery) { + return credentialQuery.getClaims() != null && !credentialQuery.getClaims().isEmpty(); + } + + private static boolean containsClaimSets(CredentialQuery credentialQuery) { + return credentialQuery.getClaimSets() != null && !credentialQuery.getClaimSets().isEmpty(); + } + + private static boolean containsMeta(CredentialQuery credentialQuery) { + return credentialQuery.getMeta() != null && !credentialQuery.getMeta().isEmpty(); + } + + private static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { + return credentialQuery.getTrustedAuthorities() != null && !credentialQuery.getTrustedAuthorities().isEmpty(); + } + + private static boolean containsCredentialSets(DcqlQuery dcqlQuery) { + return dcqlQuery.getCredentialSets() != null && !dcqlQuery.getCredentialSets().isEmpty(); + } + + private static void validateIds(List credentialQueries) { + if (credentialQueries.stream().anyMatch(cq -> cq.getId() == null)) { + throw new IllegalArgumentException("All credentialQueries need to contain an id."); + } + } + + private static Object purposeOrRandom(CredentialSetQuery credentialSetQuery) { + return Optional.ofNullable(credentialSetQuery.getPurpose()).orElse(UUID.randomUUID().toString()); + } } From 85d0ce1e72c572e95a9e8b75d985ae486153765f Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Tue, 30 Sep 2025 16:20:57 +0200 Subject: [PATCH 7/8] improvements for usage in client --- pom.xml | 2 +- .../github/wistefan/dcql/ClaimsEvaluator.java | 421 ++++++------- .../wistefan/dcql/CredentialMapper.java | 105 ++-- .../github/wistefan/dcql/DCQLEvaluator.java | 23 +- .../wistefan/dcql/EvaluationException.java | 11 + .../wistefan/dcql/model/Credential.java | 5 +- .../dcql/model/credential/CredentialBase.java | 11 + .../dcql/model/credential/Disclosure.java | 49 +- .../dcql/model/credential/JwtCredential.java | 120 ++-- .../dcql/model/credential/LdpCredential.java | 55 +- .../dcql/model/credential/MDocCredential.java | 34 +- .../model/credential/SdJwtCredential.java | 57 +- .../wistefan/dcql/ClaimsEvaluatorTest.java | 127 ++-- .../dcql/query/DcqlClaimSetQueryTest.java | 552 +++++++++--------- .../dcql/query/DcqlQueryComplexTest.java | 368 ++++++------ .../wistefan/dcql/query/DcqlQueryTest.java | 496 ++++++++-------- .../DcqlQueryTrustedAuthoritiesTest.java | 228 ++++---- .../query/DcqlQueryWithJsonTransformTest.java | 208 +++---- .../github/wistefan/dcql/query/DcqlTest.java | 184 +++--- 19 files changed, 1606 insertions(+), 1450 deletions(-) create mode 100644 src/main/java/io/github/wistefan/dcql/EvaluationException.java create mode 100644 src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java diff --git a/pom.xml b/pom.xml index 1e7da37..be42985 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,6 @@ 1.18.30 - 2.20.0 2.0.17 @@ -88,6 +87,7 @@ 5.13.4 + 2.20.0 diff --git a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java index 2697d21..af6d0a2 100644 --- a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java @@ -4,218 +4,225 @@ import io.github.wistefan.dcql.model.credential.*; import lombok.extern.slf4j.Slf4j; +import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.stream.Collectors; @Slf4j public class ClaimsEvaluator { - // key for selective disclosure values inside the VC - private static final String SD_KEY = "_sd"; - - 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(); - } - - 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.getHash())) - .toList(); - return new SdJwtCredential(credential.getJwtCredential(), cleanedDisclosures); - } - - 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(); - } - - public static Optional evaluateClaimsForLdpCredential(ClaimsQuery claimsQuery, LdpCredential credential) { - List selectedClaims = new ArrayList<>(); - try { - selectedClaims = selectClaimsByPath(credential, 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(); - } - - - public static List selectClaimsByPath(Map credential, List claimPath) { - return processPath(credential, claimPath, null); - } - - public static List selectClaimsByPathDisclosures(Map credential, List claimPath, - List disclosures) { - return processPath(credential, claimPath, disclosures); - } - - private static List processPath( - Map credential, - List claimPath, - List disclosures - ) { - if (credential == null || claimPath == null || claimPath.isEmpty()) { - throw new IllegalArgumentException("Credential and claimPath must not be null or empty"); - } - - // Start with root - List current = new ArrayList<>(); - current.add(new SelectedClaim(credential, null)); - - for (Object component : claimPath) { - List nextSelection = new ArrayList<>(); - - for (SelectedClaim candidateWrapper : current) { - Object candidate = candidateWrapper.value; - - // If map contains _sd, reveal it and MERGE revealed entries with the original map - if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey(SD_KEY)) { - Object sdObj = mapCandidate.get(SD_KEY); - Map revealed = getStringSelectedClaimMap(disclosures, sdObj); - - // Merge: start with revealed, then copy original entries (except "_sd"), - // so explicit values in the original map overwrite revealed ones if keys collide. - Map merged = new LinkedHashMap<>(); - merged.putAll(revealed); - for (Map.Entry e : mapCandidate.entrySet()) { - String k = String.valueOf(e.getKey()); - if (SD_KEY.equals(k)) continue; - merged.put(k, e.getValue()); - } - candidate = merged; - } - - // Process path component - if (component instanceof String key) { - if (!(candidate instanceof Map map)) { - throw new IllegalArgumentException("Expected object for key lookup but found: " + candidate); - } - if (map.containsKey(key)) { - Object val = map.get(key); - if (val instanceof SelectedClaim sc) { - nextSelection.add(sc); - } else { - nextSelection.add(new SelectedClaim(val, null)); - } - } - } else if (component == null) { - if (!(candidate instanceof List list)) { - throw new IllegalArgumentException("Expected array for null selector but found: " + candidate); - } - for (Object elem : list) { - if (elem instanceof SelectedClaim sc) { - nextSelection.add(sc); - } else { - nextSelection.add(new SelectedClaim(elem, null)); - } - } - } else if (component instanceof Integer index && index >= 0) { - if (!(candidate instanceof List list)) { - throw new IllegalArgumentException("Expected array for index selector but found: " + candidate); - } - if (index < list.size()) { - Object val = list.get(index); - if (val instanceof SelectedClaim sc) { - nextSelection.add(sc); - } else { - nextSelection.add(new SelectedClaim(val, null)); - } - } - } else { - throw new IllegalArgumentException("Invalid claim path component: " + component); - } - } - - if (nextSelection.isEmpty()) { - throw new IllegalArgumentException("No elements selected at path component: " + component); - } - - current = nextSelection; - } - - return current; - } - - - private static Map getStringSelectedClaimMap(List disclosures, Object sdObj) { - if (!(sdObj instanceof List sdList)) { - throw new IllegalArgumentException("_sd field must be a list"); - } - - Map revealed = new LinkedHashMap<>(); - for (Object hashObj : sdList) { - if (!(hashObj instanceof String hash)) continue; - for (Disclosure disclosure : disclosures) { - if (hash.equals(disclosure.getHash())) { - revealed.put(disclosure.getClaim(), new SelectedClaim(disclosure.getValue(), disclosure.getHash())); - } - } - } - return revealed; - } - - private record SelectedClaim(Object value, String hash) { - } + // key for selective disclosure values inside the VC + private static final String SD_KEY = "_sd"; + + 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(); + } + + 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); + } + + 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(); + } + + 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(); + } + + + public static List selectClaimsByPath(Map credential, List claimPath) { + return processPath(credential, claimPath, null); + } + + public static List selectClaimsByPathDisclosures(Map credential, List claimPath, + List disclosures) { + return processPath(credential, claimPath, disclosures); + } + + private static List processPath( + Map credential, + List claimPath, + List disclosures + ) { + if (credential == null || claimPath == null || claimPath.isEmpty()) { + throw new IllegalArgumentException("Credential and claimPath must not be null or empty"); + } + + // Start with root + List current = new ArrayList<>(); + current.add(new SelectedClaim(credential, null)); + + for (Object component : claimPath) { + List nextSelection = new ArrayList<>(); + + for (SelectedClaim candidateWrapper : current) { + Object candidate = candidateWrapper.value; + + // If map contains _sd, reveal it and MERGE revealed entries with the original map + if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey(SD_KEY)) { + Object sdObj = mapCandidate.get(SD_KEY); + Map revealed = null; + try { + revealed = getStringSelectedClaimMap(disclosures, sdObj); + } catch (NoSuchAlgorithmException e) { + throw new EvaluationException("Was not able to reveal selective disclosure.", e); + } + + // Merge: start with revealed, then copy original entries (except "_sd"), + // so explicit values in the original map overwrite revealed ones if keys collide. + Map merged = new LinkedHashMap<>(); + merged.putAll(revealed); + for (Map.Entry e : mapCandidate.entrySet()) { + String k = String.valueOf(e.getKey()); + if (SD_KEY.equals(k)) continue; + merged.put(k, e.getValue()); + } + candidate = merged; + } + + // Process path component + if (component instanceof String key) { + if (!(candidate instanceof Map map)) { + throw new IllegalArgumentException("Expected object for key lookup but found: " + candidate); + } + if (map.containsKey(key)) { + Object val = map.get(key); + if (val instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(val, null)); + } + } + } else if (component == null) { + if (!(candidate instanceof List list)) { + throw new IllegalArgumentException("Expected array for null selector but found: " + candidate); + } + for (Object elem : list) { + if (elem instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(elem, null)); + } + } + } else if (component instanceof Integer index && index >= 0) { + if (!(candidate instanceof List list)) { + throw new IllegalArgumentException("Expected array for index selector but found: " + candidate); + } + if (index < list.size()) { + Object val = list.get(index); + if (val instanceof SelectedClaim sc) { + nextSelection.add(sc); + } else { + nextSelection.add(new SelectedClaim(val, null)); + } + } + } else { + throw new IllegalArgumentException("Invalid claim path component: " + component); + } + } + + if (nextSelection.isEmpty()) { + throw new IllegalArgumentException("No elements selected at path component: " + component); + } + + current = nextSelection; + } + + return current; + } + + + private static Map getStringSelectedClaimMap(List disclosures, Object sdObj) throws NoSuchAlgorithmException { + if (!(sdObj instanceof List sdList)) { + throw new IllegalArgumentException("_sd field must be a list"); + } + + Map revealed = new LinkedHashMap<>(); + for (Object hashObj : sdList) { + if (!(hashObj instanceof String hash)) continue; + for (Disclosure disclosure : disclosures) { + String sdHash = disclosure.getSdHash(); + if (hash.equals(sdHash)) { + revealed.put(disclosure.getClaim(), new SelectedClaim(disclosure.getValue(), sdHash)); + } + } + } + return revealed; + } + + private record SelectedClaim(Object value, String hash) { + } } diff --git a/src/main/java/io/github/wistefan/dcql/CredentialMapper.java b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java index 657f5bf..66f3669 100644 --- a/src/main/java/io/github/wistefan/dcql/CredentialMapper.java +++ b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java @@ -2,67 +2,66 @@ import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; -import io.github.wistefan.dcql.model.credential.JwtCredential; -import io.github.wistefan.dcql.model.credential.LdpCredential; -import io.github.wistefan.dcql.model.credential.MDocCredential; -import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import io.github.wistefan.dcql.model.credential.*; import java.util.ArrayList; import java.util.List; public class CredentialMapper { - public static List toCredentials(CredentialFormat credentialFormat, List rawCredentials) { - return rawCredentials.stream() - .map(rC -> new Credential(credentialFormat, rC)) - .toList(); - } + public static List toCredentials(CredentialFormat credentialFormat, List rawCredentials) { + return rawCredentials.stream() + .filter(CredentialBase.class::isInstance) + .map(CredentialBase.class::cast) + .map(rC -> new Credential(credentialFormat, rC)) + .toList(); + } - public static List toLdpCredentials(List credentialsList) { - List ldpCredentialsList = new ArrayList<>(); - for (Credential c : credentialsList) { - if (c.getRawCredential() instanceof LdpCredential ldpCredential) { - ldpCredentialsList.add(ldpCredential); - } else { - throw new IllegalArgumentException("The given credential does not contain an ldp_vc."); - } - } - return ldpCredentialsList; - } + public static List toLdpCredentials(List credentialsList) { + List ldpCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof LdpCredential ldpCredential) { + ldpCredentialsList.add(ldpCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an ldp_vc."); + } + } + return ldpCredentialsList; + } - public static List toMDocCredentials(List credentialsList) { - List mDocCredentialsList = new ArrayList<>(); - for (Credential c : credentialsList) { - if (c.getRawCredential() instanceof MDocCredential mDocCredential) { - mDocCredentialsList.add(mDocCredential); - } else { - throw new IllegalArgumentException("The given credential does not contain an mso_mdoc."); - } - } - return mDocCredentialsList; - } + public static List toMDocCredentials(List credentialsList) { + List mDocCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof MDocCredential mDocCredential) { + mDocCredentialsList.add(mDocCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an mso_mdoc."); + } + } + return mDocCredentialsList; + } - public static List toJWTCredentials(List credentialsList) { - List jwtCredentialsList = new ArrayList<>(); - for (Credential c : credentialsList) { - if (c.getRawCredential() instanceof JwtCredential jwtCredential) { - jwtCredentialsList.add(jwtCredential); - } else { - throw new IllegalArgumentException("The given credential does not contain an jwt_vc_json."); - } - } - return jwtCredentialsList; - } + public static List toJWTCredentials(List credentialsList) { + List jwtCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof JwtCredential jwtCredential) { + jwtCredentialsList.add(jwtCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an jwt_vc_json."); + } + } + return jwtCredentialsList; + } - public static List toSdJWTCredentials(List credentialsList) { - List sdJwtCredentialsList = new ArrayList<>(); - for (Credential c : credentialsList) { - if (c.getRawCredential() instanceof SdJwtCredential sdJWTCredential) { - sdJwtCredentialsList.add(sdJWTCredential); - } else { - throw new IllegalArgumentException("The given credential does not contain an vc+sd-jwt/dc+sd-jwt."); - } - } - return sdJwtCredentialsList; - } + public static List toSdJWTCredentials(List credentialsList) { + List sdJwtCredentialsList = new ArrayList<>(); + for (Credential c : credentialsList) { + if (c.getRawCredential() instanceof SdJwtCredential sdJWTCredential) { + sdJwtCredentialsList.add(sdJWTCredential); + } else { + throw new IllegalArgumentException("The given credential does not contain an vc+sd-jwt/dc+sd-jwt."); + } + } + return sdJwtCredentialsList; + } } diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java index a41ba2f..403eb5f 100644 --- a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -107,7 +107,7 @@ private static List evaluateForSdJwt(CredentialQuery credentialQuery } else { sdJwtCredentials = sdJwtCredentials.stream() // keep the original credential untouched - .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getJwtCredential(), List.of())) + .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getRaw(), sdJwtCredential.getJwtCredential(), List.of())) .toList(); } return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); @@ -192,7 +192,7 @@ private static List evaluateSdJwtForClaimSet(CredentialQuery credent .orElse(new ArrayList<>()))); } if (!disclosures.isEmpty()) { - disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(disclosures))); + disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(disclosures))); } } @@ -234,7 +234,7 @@ private static List evaluateSdJwtCredentialsQuery(CredentialQue .map(SdJwtCredential::getDisclosures) .flatMap(List::stream) .collect(Collectors.toSet()); - disclosedCredentials.add(new SdJwtCredential(credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); + disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); } return disclosedCredentials; } @@ -309,9 +309,12 @@ private static List filterSdJwtByMetadata(Map m } private static List filterJwtByMetadata(Map metaData, List credentialsList) { - JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); return credentialsList.stream() - .filter(jwtCredential -> jwtMetaData.getVctValues().contains(jwtCredential.getVct())) + .filter(jwtCredential -> + w3CMetaData.getTypeValues() + .stream() + .anyMatch(metaTypes -> new HashSet<>(jwtCredential.getType()).containsAll(metaTypes))) .toList(); } @@ -322,23 +325,23 @@ private static List filterMDocByMetadata(Map met .toList(); } - private static boolean containsClaims(CredentialQuery credentialQuery) { + public static boolean containsClaims(CredentialQuery credentialQuery) { return credentialQuery.getClaims() != null && !credentialQuery.getClaims().isEmpty(); } - private static boolean containsClaimSets(CredentialQuery credentialQuery) { + public static boolean containsClaimSets(CredentialQuery credentialQuery) { return credentialQuery.getClaimSets() != null && !credentialQuery.getClaimSets().isEmpty(); } - private static boolean containsMeta(CredentialQuery credentialQuery) { + public static boolean containsMeta(CredentialQuery credentialQuery) { return credentialQuery.getMeta() != null && !credentialQuery.getMeta().isEmpty(); } - private static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { + public static boolean containsTrustAuthorities(CredentialQuery credentialQuery) { return credentialQuery.getTrustedAuthorities() != null && !credentialQuery.getTrustedAuthorities().isEmpty(); } - private static boolean containsCredentialSets(DcqlQuery dcqlQuery) { + public static boolean containsCredentialSets(DcqlQuery dcqlQuery) { return dcqlQuery.getCredentialSets() != null && !dcqlQuery.getCredentialSets().isEmpty(); } diff --git a/src/main/java/io/github/wistefan/dcql/EvaluationException.java b/src/main/java/io/github/wistefan/dcql/EvaluationException.java new file mode 100644 index 0000000..aeeab84 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/EvaluationException.java @@ -0,0 +1,11 @@ +package io.github.wistefan.dcql; + +public class EvaluationException extends RuntimeException { + public EvaluationException(String message) { + super(message); + } + + public EvaluationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/Credential.java b/src/main/java/io/github/wistefan/dcql/model/Credential.java index 07ce200..9e84e07 100644 --- a/src/main/java/io/github/wistefan/dcql/model/Credential.java +++ b/src/main/java/io/github/wistefan/dcql/model/Credential.java @@ -1,5 +1,6 @@ package io.github.wistefan.dcql.model; +import io.github.wistefan.dcql.model.credential.CredentialBase; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -9,6 +10,6 @@ @NoArgsConstructor public class Credential { - private CredentialFormat credentialFormat; - private Object rawCredential; + private CredentialFormat credentialFormat; + private CredentialBase rawCredential; } diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java b/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java new file mode 100644 index 0000000..3d2b57c --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java @@ -0,0 +1,11 @@ +package io.github.wistefan.dcql.model.credential; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public abstract class CredentialBase { + + protected final String raw; +} diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java index 703f095..1aeed0e 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java @@ -1,16 +1,47 @@ package io.github.wistefan.dcql.model.credential; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; +import io.github.wistefan.dcql.EvaluationException; +import lombok.*; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; -@AllArgsConstructor @Data -@NoArgsConstructor @EqualsAndHashCode public class Disclosure { - private String hash; - private String claim; - private Object value; + private String salt; + private String claim; + private Object value; + // the plain, encoded disclosure as it was provided in the original credential + private final String encodedDisclosure; + private final String sdHash; + + public Disclosure(String salt, String claim, Object value, String encodedDisclosure, String sdAlgorithm) { + this.salt = salt; + this.claim = claim; + this.value = value; + this.encodedDisclosure = encodedDisclosure; + this.sdHash = generateSdHash(sdAlgorithm); + } + + private String generateSdHash(String sdAlgorithm) { + byte[] disclosureBytes = encodedDisclosure.getBytes(StandardCharsets.UTF_8); + MessageDigest digest = getMessageDigest(sdAlgorithm); + + byte[] hash = digest.digest(disclosureBytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } + + private MessageDigest getMessageDigest(String sdAlgorithm) { + if (sdAlgorithm.equalsIgnoreCase("SHA-256")) { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new EvaluationException(String.format("SD-Algorithm %s is not supported.", sdAlgorithm), e); + } + } + throw new EvaluationException(String.format("SD-Algorithm %s is not supported.", sdAlgorithm)); + } } diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java index cab10bc..055601d 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java @@ -1,63 +1,75 @@ package io.github.wistefan.dcql.model.credential; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.extern.slf4j.Slf4j; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; -@Data -@AllArgsConstructor -@NoArgsConstructor -public class JwtCredential { - - private static final String VCT_KEY = "vct"; - private static final String TYPE_KEY = "type"; - private static final String X5C_KEY = "x5c"; - - private Map headers; - private Map payload; - private String signature; - - public List getX5Chain() { - if (headers.containsKey(X5C_KEY) && headers.get(X5C_KEY) instanceof List x5Chain) { - List x509Certificates = x5Chain.stream() - .filter(X509Certificate.class::isInstance) - .map(X509Certificate.class::cast) - .toList(); - if (x5Chain.size() != x509Certificates.size()) { - throw new IllegalArgumentException("The x5c header contains invalid values."); - } - return x509Certificates; - } - // a x5c-header is not mandatory, thus an empty list is completely valid. - return List.of(); - } - - public List getType() { - if (payload.containsKey(TYPE_KEY)) { - if (payload.get(TYPE_KEY) instanceof String typeString) { - return List.of(typeString); - } else if (payload.get(TYPE_KEY) instanceof List typeList) { - List typeStrings = typeList.stream() - .filter(String.class::isInstance) - .map(String.class::cast) - .toList(); - if (typeStrings.size() == typeList.size()) { - return typeStrings; - } - } - } - throw new IllegalArgumentException("The type field contains invalid entries."); - } - - public String getVct() { - if (payload.containsKey(VCT_KEY) && payload.get(VCT_KEY) instanceof String vctValue) { - return vctValue; - } - throw new IllegalArgumentException("Invalid credential. Does not contain a valid vct."); - } +@Getter +public class JwtCredential extends CredentialBase { + + private static final String VC_PAYLOAD_KEY = "vc"; + private static final String VCT_KEY = "vct"; + private static final String TYPE_KEY = "type"; + private static final String X5C_KEY = "x5c"; + + private Map headers; + private Map payload; + private String signature; + + public JwtCredential(String raw, Map headers, Map payload, String signature) { + super(raw); + this.headers = headers; + this.payload = payload; + this.signature = signature; + } + + public List getX5Chain() { + if (headers.containsKey(X5C_KEY) && headers.get(X5C_KEY) instanceof List x5Chain) { + List x509Certificates = x5Chain.stream() + .filter(X509Certificate.class::isInstance) + .map(X509Certificate.class::cast) + .toList(); + if (x5Chain.size() != x509Certificates.size()) { + throw new IllegalArgumentException("The x5c header contains invalid values."); + } + return x509Certificates; + } + // a x5c-header is not mandatory, thus an empty list is completely valid. + return List.of(); + } + + public Map getPayload() { + if (payload.containsKey(VC_PAYLOAD_KEY)) { + return (Map) payload.get(VC_PAYLOAD_KEY); + } + return payload; + } + + public List getType() { + if (getPayload().containsKey(TYPE_KEY)) { + if (getPayload().get(TYPE_KEY) instanceof String typeString) { + return List.of(typeString); + } else if (getPayload().get(TYPE_KEY) instanceof List typeList) { + List typeStrings = typeList.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (typeStrings.size() == typeList.size()) { + return typeStrings; + } + } + } + throw new IllegalArgumentException("The type field contains invalid entries."); + } + + public String getVct() { + if (getPayload().containsKey(VCT_KEY) && getPayload().get(VCT_KEY) instanceof String vctValue) { + return vctValue; + } + throw new IllegalArgumentException("Invalid credential. Does not contain a valid vct."); + } } diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java index f7c45af..2a9dd12 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java @@ -1,32 +1,37 @@ package io.github.wistefan.dcql.model.credential; -import java.util.HashMap; +import lombok.Getter; + import java.util.List; import java.util.Map; -public class LdpCredential extends HashMap { - - private static final String TYPE_KEY = "type"; - - public LdpCredential(Map m) { - super(m); - } - - public List getType() { - if (this.containsKey(TYPE_KEY)) { - if (this.get(TYPE_KEY) instanceof String typeString) { - return List.of(typeString); - } else if (this.get(TYPE_KEY) instanceof List typeList) { - List typeStrings = typeList.stream() - .filter(String.class::isInstance) - .map(String.class::cast) - .toList(); - if (typeStrings.size() == typeList.size()) { - return typeStrings; - } - } - } - throw new IllegalArgumentException("The type field contains invalid entries."); - } +@Getter +public class LdpCredential extends CredentialBase { + + private static final String TYPE_KEY = "type"; + + private final Map theCredential; + + public LdpCredential(String raw, Map theCredential) { + super(raw); + this.theCredential = theCredential; + } + + public List getType() { + if (theCredential.containsKey(TYPE_KEY)) { + if (theCredential.get(TYPE_KEY) instanceof String typeString) { + return List.of(typeString); + } else if (theCredential.get(TYPE_KEY) instanceof List typeList) { + List typeStrings = typeList.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (typeStrings.size() == typeList.size()) { + return typeStrings; + } + } + } + throw new IllegalArgumentException("The type field contains invalid entries."); + } } diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java index 1e4fba0..9b0e3bb 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java @@ -1,25 +1,27 @@ package io.github.wistefan.dcql.model.credential; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Getter; import java.util.Map; -@AllArgsConstructor -@Data -@NoArgsConstructor -public class MDocCredential { +@Getter +public class MDocCredential extends CredentialBase { - private static final String DOC_TYPE_KEY = "docType"; + private static final String DOC_TYPE_KEY = "docType"; - private MDocHeaders headers; - private Map payload; + private MDocHeaders headers; + private Map payload; - public String getDocType() { - if (payload.containsKey(DOC_TYPE_KEY) && payload.get(DOC_TYPE_KEY) instanceof String docType) { - return docType; - } - throw new IllegalArgumentException("The credential does not contain a valid docType."); - } + public MDocCredential(String raw, MDocHeaders headers, Map payload) { + super(raw); + this.headers = headers; + this.payload = payload; + } + + public String getDocType() { + if (payload.containsKey(DOC_TYPE_KEY) && payload.get(DOC_TYPE_KEY) instanceof String docType) { + return docType; + } + throw new IllegalArgumentException("The credential does not contain a valid docType."); + } } diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java index 6ebed83..3f967c3 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java @@ -1,24 +1,51 @@ package io.github.wistefan.dcql.model.credential; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Getter; +import java.util.Base64; import java.util.List; +import java.util.StringJoiner; -@AllArgsConstructor -@Data -@NoArgsConstructor -public class SdJwtCredential { +@Getter +public class SdJwtCredential extends CredentialBase { - private JwtCredential jwtCredential; - private List disclosures; + private static final String SD_JWT_SEPERATOR = "~"; - public String getVct() { - return jwtCredential.getVct(); - } + private JwtCredential jwtCredential; + private List disclosures; + + public SdJwtCredential(String raw, JwtCredential jwtCredential, List disclosures) { + super(raw); + this.jwtCredential = jwtCredential; + this.disclosures = disclosures; + } + + /** + * For SD-JWT Credentials we cannot return the full raw-credential, since we might disclose claims that are not requested. + * Instead, the "raw" needs to be rebuilt from the jwt-part and the selected disclosures. + */ + @Override + public String getRaw() { + if (raw == null) { + return null; + } + String[] splittedRaw = super.getRaw().split(SD_JWT_SEPERATOR); + StringJoiner sdJoiner = new StringJoiner(SD_JWT_SEPERATOR); + // first element is the plain jwt. + sdJoiner.add(splittedRaw[0]); + disclosures.stream() + .map(Disclosure::getEncodedDisclosure) + .forEach(sdJoiner::add); + // the sd needs to end with an ~ + return sdJoiner + SD_JWT_SEPERATOR; + } + + public String getVct() { + return jwtCredential.getVct(); + } + + public List getType() { + return jwtCredential.getType(); + } - public List getType() { - return jwtCredential.getType(); - } } diff --git a/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java b/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java index 4d07649..9fc42b0 100644 --- a/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java +++ b/src/test/java/io/github/wistefan/dcql/ClaimsEvaluatorTest.java @@ -4,6 +4,7 @@ import io.github.wistefan.dcql.model.credential.Disclosure; import io.github.wistefan.dcql.model.credential.JwtCredential; import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import io.github.wistefan.dcql.query.DcqlTest; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -18,60 +19,84 @@ class ClaimsEvaluatorTest { - @ParameterizedTest - @MethodSource("jwtArgs") - public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map credential, boolean expectedResult) { - JwtCredential jwtCredential = new JwtCredential(null, credential, null); - assertEquals(expectedResult, ClaimsEvaluator.evaluateClaimsForJwtCredential(claimsQuery, jwtCredential).isPresent()); - } + @ParameterizedTest + @MethodSource("jwtArgs") + public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map credential, boolean expectedResult) { + JwtCredential jwtCredential = new JwtCredential( null, null, credential, null); + assertEquals(expectedResult, ClaimsEvaluator.evaluateClaimsForJwtCredential(claimsQuery, jwtCredential).isPresent()); + } - @ParameterizedTest - @MethodSource("sdJwtArgs") - public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map payload, List disclosures, Optional> expectedDisclosures) { + @ParameterizedTest + @MethodSource("sdJwtArgs") + public void testEvaluateForJwtCredential(ClaimsQuery claimsQuery, Map payload, List disclosures, Optional> expectedDisclosures) { - SdJwtCredential sdJwtCredential = new SdJwtCredential(new JwtCredential(null, payload, null), disclosures); - Optional optionalSdJwtCredential = ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, sdJwtCredential); + SdJwtCredential sdJwtCredential = new SdJwtCredential( null, new JwtCredential( null, null, payload, null), disclosures); + Optional optionalSdJwtCredential = ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, sdJwtCredential); - assertEquals(expectedDisclosures.isPresent(), optionalSdJwtCredential.isPresent()); - expectedDisclosures.ifPresent(disclosureList -> assertEquals(disclosureList, optionalSdJwtCredential.get().getDisclosures())); - } + assertEquals(expectedDisclosures.isPresent(), optionalSdJwtCredential.isPresent()); + expectedDisclosures.ifPresent(disclosureList -> assertEquals(disclosureList, optionalSdJwtCredential.get().getDisclosures())); + } - public static Stream sdJwtArgs() { - return Stream.of( - Arguments.of( - new ClaimsQuery("id", List.of("test", "a"), null), - Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), - List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), - Optional.of(List.of(new Disclosure("hash-a", "a", "b")))), - Arguments.of( - new ClaimsQuery("id", List.of("test", "a"), List.of("b")), - Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), - List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), - Optional.of(List.of(new Disclosure("hash-a", "a", "b")))), - Arguments.of( - new ClaimsQuery("id", List.of("test", "a"), List.of("c")), - Map.of("test", Map.of("_sd", List.of("hash-a", "hash-b"))), - List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d")), - Optional.empty()), - Arguments.of( - new ClaimsQuery("id", List.of("test","e"), List.of("f")), - Map.of("_sd", List.of("hash-a", "hash-b"), "test",Map.of("_sd", List.of("hash-c"))), - List.of(new Disclosure("hash-a", "a", "b"), new Disclosure("hash-b", "c", "d"), new Disclosure("hash-c", "e", "f")), - Optional.of(List.of(new Disclosure("hash-c", "e", "f")))) - ); - } + public static Stream sdJwtArgs() { + return Stream.of( - public static Stream jwtArgs() { - List nullList = new ArrayList<>(); - nullList.add("test"); - nullList.add(null); - nullList.add("a"); - return Stream.of( - Arguments.of(new ClaimsQuery("id", nullList, null), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), true), - Arguments.of(new ClaimsQuery("id", nullList, List.of("c")), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), false), - Arguments.of(new ClaimsQuery("id", List.of("test", "a"), null), Map.of("test", Map.of("a", "b", "c", "d")), true), - Arguments.of(new ClaimsQuery("id", List.of("test", "d"), null), Map.of("test", Map.of("a", "b", "c", "d")), false), - Arguments.of(new ClaimsQuery("id", List.of("test", "a"), List.of("b")), Map.of("test", Map.of("a", "b", "c", "d")), true) - ); - } + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), null), + Map.of("test", Map.of("_sd", + List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash(), "decoy"))), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), null), + Map.of("test", Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash()))), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), List.of("b")), + Map.of("test", Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash()))), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "a"), List.of("c")), + Map.of("test", Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash()))), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.empty()), + Arguments.of( + new ClaimsQuery("id", List.of("a"), List.of("b")), + Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash())), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("a"), null), + Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), + DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash())), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-a", "a", "b")))), + Arguments.of( + new ClaimsQuery("id", List.of("test", "e"), List.of("f")), + Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-a", "a", "b").getSdHash(), DcqlTest.getDisclosure("hash-b", "c", "d").getSdHash()), + "test", Map.of("_sd", List.of(DcqlTest.getDisclosure("hash-c", "e", "f").getSdHash()))), + List.of(DcqlTest.getDisclosure("hash-a", "a", "b"), DcqlTest.getDisclosure("hash-b", "c", "d"), DcqlTest.getDisclosure("hash-c", "e", "f")), + Optional.of(List.of(DcqlTest.getDisclosure("hash-c", "e", "f")))) + ); + } + + public static Stream jwtArgs() { + List nullList = new ArrayList<>(); + nullList.add("test"); + nullList.add(null); + nullList.add("a"); + return Stream.of( + Arguments.of(new ClaimsQuery("id", nullList, null), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), true), + Arguments.of(new ClaimsQuery("id", nullList, List.of("c")), Map.of("test", List.of(Map.of("a", "b"), Map.of("a", "d"))), false), + Arguments.of(new ClaimsQuery("id", List.of("test", "a"), null), Map.of("test", Map.of("a", "b", "c", "d")), true), + Arguments.of(new ClaimsQuery("id", List.of("test", "d"), null), Map.of("test", Map.of("a", "b", "c", "d")), false), + Arguments.of(new ClaimsQuery("id", List.of("test", "a"), List.of("b")), Map.of("test", Map.of("a", "b", "c", "d")), true) + ); + } } \ No newline at end of file diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java index 895683c..38f50bf 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java @@ -1,6 +1,7 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.Base64; import java.util.List; import java.util.Map; @@ -17,276 +19,282 @@ public class DcqlClaimSetQueryTest extends DcqlTest { - private static final String MDOC_MVRC_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "mso_mdoc", - "multiple": true, - "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, - "claims": [ - { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, - { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, - { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } - ], - "claim_sets": [ - ["b","c"], - ["a"] - ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final String MDOC_MVRC_QUERY_SINGLE = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "mso_mdoc", - "multiple": false, - "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, - "claims": [ - { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, - { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, - { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } - ], - "claim_sets": [ - ["b","c"], - ["a"] - ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final Credential MDOC_MVRC_FULL = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), - "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") - ), - "authority", Map.of("type", "aki", "values", List.of("one")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MVRC_HOLDER = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer") - ), - "authority", Map.of("type", "aki", "values", List.of("one")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MVRC_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") - ), - "authority", Map.of("type", "aki", "values", List.of("one")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MVRC_LAST_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.18013.5.1", Map.of("last_name", "Auer") - ), - "authority", Map.of("type", "aki", "values", List.of("one")), - "cryptographic_holder_binding", true - ))); - - - private static final String SD_JWT_QUERY_ADDRESS = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "vc+sd-jwt", - "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential"] }, - "claims": [ - { "id": "a", "path": ["address","street_address"] }, - { "id": "b", "path": ["street_address"] } - ], - "claim_sets": [ - ["b"], - ["a"] - ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final String SD_JWT_QUERY_ALTERNATIVES = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "vc+sd-jwt", - "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential","https://credentials.example.com/name_credential"] }, - "claims": [ - { "id": "a", "path": ["address","street_address"] }, - { "id": "b", "path": ["street_address"] }, - { "id": "c", "path": ["first_name"] }, - { "id": "d", "path": ["last_name"] } - ], - "claim_sets": [ - ["c","d"], - ["b"], - ["a"] - ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - private static final Credential SD_JWT_VC_FULL = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( - new JwtCredential(null, - Map.of( - "vct", "https://credentials.example.com/identity_credential", - "name", Map.of("_sd", List.of("hash-b", "hash-c")), - "address", Map.of("_sd", List.of("hash-a", "hash-x")), - "cryptographic_holder_binding", false), null), - List.of(new Disclosure("hash-a", "street_address", "42 Market Street"), - new Disclosure("hash-b", "first_name", "Arthur"), - new Disclosure("hash-c", "last_name", "Dent")) - )); - - private static final Credential SD_JWT_VC_ADDRESS = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( - new JwtCredential(null, - Map.of( - "vct", "https://credentials.example.com/address_credential", - "_sd", List.of("hash-a", "hash-x"), - "cryptographic_holder_binding", false), null), - List.of(new Disclosure("hash-a", "street_address", "42 Market Street")) - )); - - private static final Credential SD_JWT_VC_NAME = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( - new JwtCredential(null, - Map.of( - "vct", "https://credentials.example.com/name_credential", - "_sd", List.of("hash-b", "hash-c"), - "cryptographic_holder_binding", false), null), - List.of(new Disclosure("hash-b", "first_name", "Arthur"), - new Disclosure("hash-c", "last_name", "Dent")) - )); - - @Test - @DisplayName("sd-jwt query get alternative") - void sdJwtQueryGetAlternative() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(1, sdJwtCredential.getDisclosures().size()); - } else { - fail("Did not get an SdJwt Credential."); - } - } - - - @Test - @DisplayName("sd-jwt query get for name") - void sdJwtQueryForName() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(2, sdJwtCredential.getDisclosures().size()); - } else { - fail("Did not get an SdJwt Credential."); - } - } - - @Test - @DisplayName("sd-jwt query get for street_address within full") - void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(1, sdJwtCredential.getDisclosures().size()); - } else { - fail("Did not get an SdJwt Credential."); - } - } - - @Test - @DisplayName("sd-jwt query get for street_address") - void sdJwtQueryForStreetAddress() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(1, sdJwtCredential.getDisclosures().size()); - } else { - fail("Did not get an SdJwt Credential."); - } - } - - @Test - @DisplayName("mdoc mvrc query get full doc") - void mdocMvrcQueryFullDocSet() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - assertEquals(credential, MDOC_MVRC_FULL); - } - - @Test - @DisplayName("mdoc mvrc query get second set") - void mdocMvrcQuerySecondSet() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - assertEquals(credential, MDOC_MVRC_HOLDER); - } - - @Test - @DisplayName("mdoc mvrc query gets the fullfilling credentials.") - void mdocMvrcQueryOnlyOne() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); - - assertTrue(queryResult.success()); - assertEquals(2, queryResult.credentials().get("credentials").size()); - List credentials = queryResult.credentials().get("credentials"); - assertTrue(credentials.contains(MDOC_MVRC_NAME)); - assertTrue(credentials.contains(MDOC_MVRC_FULL)); - } - - @Test - @DisplayName("mdoc mvrc query fails when multiple credentials match, but multiple is not allowed.") - void mdocMvrcQueryFailedMultiple() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY_SINGLE, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); - - assertFalse(queryResult.success()); - } + private static final String MDOC_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "multiple": true, + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, + { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "claim_sets": [ + ["b","c"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String MDOC_MVRC_QUERY_SINGLE = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "multiple": false, + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "id": "a", "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "id": "b", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" }, + { "id": "c", "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "claim_sets": [ + ["b","c"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final Credential MDOC_MVRC_FULL = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), + "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_HOLDER = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_LAST_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.18013.5.1", Map.of("last_name", "Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + + private static final String SD_JWT_QUERY_ADDRESS = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential"] }, + "claims": [ + { "id": "a", "path": ["address","street_address"] }, + { "id": "b", "path": ["street_address"] } + ], + "claim_sets": [ + ["b"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_QUERY_ALTERNATIVES = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential", "https://credentials.example.com/address_credential","https://credentials.example.com/name_credential"] }, + "claims": [ + { "id": "a", "path": ["address","street_address"] }, + { "id": "b", "path": ["street_address"] }, + { "id": "c", "path": ["first_name"] }, + { "id": "d", "path": ["last_name"] } + ], + "claim_sets": [ + ["c","d"], + ["b"], + ["a"] + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + private static final Credential SD_JWT_VC_FULL = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( null, + new JwtCredential( null, null, + Map.of( + "vct", "https://credentials.example.com/identity_credential", + "name", Map.of("_sd", List.of(getDisclosure("salt-b", "first_name", "Arthur").getSdHash(), getDisclosure("salt-c", "last_name", "Dent").getSdHash())), + "address", Map.of("_sd", List.of(getDisclosure("salt-a", "street_address", "42 Market Street").getSdHash(), "hash-x")), + "cryptographic_holder_binding", false), null), + List.of(getDisclosure("salt-a", "street_address", "42 Market Street"), + getDisclosure("salt-b", "first_name", "Arthur"), + getDisclosure("salt-c", "last_name", "Dent")) + )); + + private static final Credential SD_JWT_VC_ADDRESS = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( null, + new JwtCredential( null, null, + Map.of( + "vct", "https://credentials.example.com/address_credential", + "_sd", List.of( + getDisclosure("salt-a", "street_address", "42 Market Street") + .getSdHash(), + "hash-x"), + "cryptographic_holder_binding", false), null), + List.of(getDisclosure("salt-a", "street_address", "42 Market Street")) + )); + + private static final Credential SD_JWT_VC_NAME = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( null, + new JwtCredential( null, null, + Map.of( + "vct", "https://credentials.example.com/name_credential", + "_sd", List.of( + getDisclosure("salt-b", "first_name", "Arthur").getSdHash(), + getDisclosure("salt-c", "last_name", "Dent").getSdHash()), + "cryptographic_holder_binding", false), null), + List.of(getDisclosure("salt-b", "first_name", "Arthur"), + getDisclosure("salt-c", "last_name", "Dent")) + )); + + + @Test + @DisplayName("sd-jwt query get alternative") + void sdJwtQueryGetAlternative() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + + @Test + @DisplayName("sd-jwt query get for name") + void sdJwtQueryForName() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(2, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("sd-jwt query get for street_address within full") + void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("sd-jwt query get for street_address") + void sdJwtQueryForStreetAddress() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(1, sdJwtCredential.getDisclosures().size()); + } else { + fail("Did not get an SdJwt Credential."); + } + } + + @Test + @DisplayName("mdoc mvrc query get full doc") + void mdocMvrcQueryFullDocSet() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(credential, MDOC_MVRC_FULL); + } + + @Test + @DisplayName("mdoc mvrc query get second set") + void mdocMvrcQuerySecondSet() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(credential, MDOC_MVRC_HOLDER); + } + + @Test + @DisplayName("mdoc mvrc query gets the fullfilling credentials.") + void mdocMvrcQueryOnlyOne() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + + assertTrue(queryResult.success()); + assertEquals(2, queryResult.credentials().get("credentials").size()); + List credentials = queryResult.credentials().get("credentials"); + assertTrue(credentials.contains(MDOC_MVRC_NAME)); + assertTrue(credentials.contains(MDOC_MVRC_FULL)); + } + + @Test + @DisplayName("mdoc mvrc query fails when multiple credentials match, but multiple is not allowed.") + void mdocMvrcQueryFailedMultiple() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY_SINGLE, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + + assertFalse(queryResult.success()); + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java index cc3da83..4ba97ef 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java @@ -20,189 +20,189 @@ class DcqlQueryComplexTest extends DcqlTest { - // --- Test Data --- - - private static final String COMPLEX_MDOC_QUERY = """ - { - "credentials": [ - { - "id": "mdl-id", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, - "claims": [ - { "id": "given_name", "namespace": "org.iso.18013.5.1", "claim_name": "given_name" }, - { "id": "family_name", "namespace": "org.iso.18013.5.1", "claim_name": "family_name" }, - { "id": "portrait", "namespace": "org.iso.18013.5.1", "claim_name": "portrait" } - ] - }, - { - "id": "mdl-address", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, - "claims": [ - { "id": "resident_address", "path": ["org.iso.18013.5.1", "resident_address"], "intent_to_retain": false }, - { "id": "resident_country", "path": ["org.iso.18013.5.1", "resident_country"], "intent_to_retain": true } - ] - }, - { - "id": "photo_card-id", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.23220.photoid.1" }, - "claims": [ - { "id": "given_name", "path": ["org.iso.23220.1", "given_name"] }, - { "id": "family_name", "path": ["org.iso.23220.1", "family_name"] }, - { "id": "portrait", "path": ["org.iso.23220.1", "portrait"] } - ] - }, - { - "id": "photo_card-address", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.23220.photoid.1" }, - "claims": [ - { "id": "resident_address", "path": ["org.iso.23220.1", "resident_address"] }, - { "id": "resident_country", "path": ["org.iso.23220.1", "resident_country"] } - ] - } - ], - "credential_sets": [ - { "purpose": "Identification", "options": [["mdl-id"], ["photo_card-id"]] }, - { "purpose": "Proof of address", "required": false, "options": [["mdl-address"], ["photo_card-address"]] } - ] - } - """; - - private static final Credential MDOC_MDL_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "credential_format", "mso_mdoc", - "docType", "org.iso.18013.5.1.mDL", - "namespaces", Map.of("org.iso.18013.5.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MDL_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "credential_format", "mso_mdoc", - "docType", "org.iso.18013.5.1.mDL", - "namespaces", Map.of("org.iso.18013.5.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_PHOTO_CARD_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "credential_format", "mso_mdoc", - "docType", "org.iso.23220.photoid.1", - "namespaces", Map.of("org.iso.23220.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_PHOTO_CARD_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "credential_format", "mso_mdoc", - "docType", "org.iso.23220.photoid.1", - "namespaces", Map.of("org.iso.23220.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_EXAMPLE = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "credential_format", "mso_mdoc", - "docType", "example_doctype", - "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), - "cryptographic_holder_binding", true - ))); - - private static final Credential SD_JWT_VC_EXAMPLE = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential( - new JwtCredential(null, Map.of( - "credential_format", "vc+sd-jwt", - "vct", "https://credentials.example.com/identity_credential", - "claims", Map.of( - "first_name", "Arthur", - "last_name", "Dent", - "address", Map.of("street_address", "42 Market Street", "locality", "Milliways", "postal_code", "12345"), - "degrees", List.of( - Map.of("type", "Bachelor of Science", "university", "University of Betelgeuse"), - Map.of("type", "Master of Science", "university", "University of Betelgeuse") - ), - "nationalities", List.of("British", "Betelgeusian") - ), - "cryptographic_holder_binding", true - ), null), List.of())); - - - @Test - @DisplayName("fails with no credentials") - void failsWithNoCredentials() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); - - assertFalse(queryResult.success()); - } - - @Test - @DisplayName("fails with credentials that do not satisfy a required claim_set") - void failsWithCredentialsThatDoNotSatisfyARequiredClaimSet() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); - - assertFalse(queryResult.success()); - } - - @Test - @DisplayName("return the requested sets") - void succeedsWithRequestedSets() throws JsonProcessingException { - List expectedIdCredentials = List.of(MDOC_MDL_ID); - List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); - - var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( - MDOC_MDL_ID, - MDOC_MDL_ADDRESS, - MDOC_PHOTO_CARD_ID, - MDOC_PHOTO_CARD_ADDRESS, - MDOC_EXAMPLE, - SD_JWT_VC_EXAMPLE)); - - assertTrue(queryResult.success()); - assertTrue(queryResult.credentials().containsKey("Identification")); - assertTrue(queryResult.credentials().containsKey("Proof of address")); - - List identification = queryResult.credentials().get("Identification"); - List poa = queryResult.credentials().get("Proof of address"); - - assertEquals(1, identification.size()); - assertEquals(1, poa.size()); - - expectedIdCredentials.forEach( - ec -> assertTrue(identification.contains(ec))); - expectedPoaCredentials.forEach( - ec -> assertTrue(poa.contains(ec))); - } - - @Test - @DisplayName("return alternative if not included") - void returnAlternative() throws JsonProcessingException { - List expectedIdCredentials = List.of(MDOC_PHOTO_CARD_ID); - List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); - - var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( - MDOC_MDL_ADDRESS, - MDOC_PHOTO_CARD_ID, - MDOC_PHOTO_CARD_ADDRESS, - MDOC_EXAMPLE, - SD_JWT_VC_EXAMPLE)); - - assertTrue(queryResult.success()); - assertTrue(queryResult.credentials().containsKey("Identification")); - assertTrue(queryResult.credentials().containsKey("Proof of address")); - - List identification = queryResult.credentials().get("Identification"); - List poa = queryResult.credentials().get("Proof of address"); - - assertEquals(1, identification.size()); - assertEquals(1, poa.size()); - - expectedIdCredentials.forEach( - ec -> assertTrue(identification.contains(ec))); - expectedPoaCredentials.forEach( - ec -> assertTrue(poa.contains(ec))); - } + // --- Test Data --- + + private static final String COMPLEX_MDOC_QUERY = """ + { + "credentials": [ + { + "id": "mdl-id", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, + "claims": [ + { "id": "given_name", "namespace": "org.iso.18013.5.1", "claim_name": "given_name" }, + { "id": "family_name", "namespace": "org.iso.18013.5.1", "claim_name": "family_name" }, + { "id": "portrait", "namespace": "org.iso.18013.5.1", "claim_name": "portrait" } + ] + }, + { + "id": "mdl-address", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.18013.5.1.mDL" }, + "claims": [ + { "id": "resident_address", "path": ["org.iso.18013.5.1", "resident_address"], "intent_to_retain": false }, + { "id": "resident_country", "path": ["org.iso.18013.5.1", "resident_country"], "intent_to_retain": true } + ] + }, + { + "id": "photo_card-id", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.23220.photoid.1" }, + "claims": [ + { "id": "given_name", "path": ["org.iso.23220.1", "given_name"] }, + { "id": "family_name", "path": ["org.iso.23220.1", "family_name"] }, + { "id": "portrait", "path": ["org.iso.23220.1", "portrait"] } + ] + }, + { + "id": "photo_card-address", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.23220.photoid.1" }, + "claims": [ + { "id": "resident_address", "path": ["org.iso.23220.1", "resident_address"] }, + { "id": "resident_country", "path": ["org.iso.23220.1", "resident_country"] } + ] + } + ], + "credential_sets": [ + { "purpose": "Identification", "options": [["mdl-id"], ["photo_card-id"]] }, + { "purpose": "Proof of address", "required": false, "options": [["mdl-address"], ["photo_card-address"]] } + ] + } + """; + + private static final Credential MDOC_MDL_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.18013.5.1.mDL", + "namespaces", Map.of("org.iso.18013.5.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MDL_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.18013.5.1.mDL", + "namespaces", Map.of("org.iso.18013.5.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_PHOTO_CARD_ID = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.23220.photoid.1", + "namespaces", Map.of("org.iso.23220.1", Map.of("given_name", "Martin", "family_name", "Auer", "portrait", "https://example.com/portrait")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_PHOTO_CARD_ADDRESS = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "credential_format", "mso_mdoc", + "docType", "org.iso.23220.photoid.1", + "namespaces", Map.of("org.iso.23220.1", Map.of("resident_country", "Italy", "resident_address", "Via Roma 1", "non_disclosed", "secret")), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_EXAMPLE = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "credential_format", "mso_mdoc", + "docType", "example_doctype", + "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), + "cryptographic_holder_binding", true + ))); + + private static final Credential SD_JWT_VC_EXAMPLE = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential( null, + new JwtCredential( null, null, Map.of( + "credential_format", "vc+sd-jwt", + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of( + "first_name", "Arthur", + "last_name", "Dent", + "address", Map.of("street_address", "42 Market Street", "locality", "Milliways", "postal_code", "12345"), + "degrees", List.of( + Map.of("type", "Bachelor of Science", "university", "University of Betelgeuse"), + Map.of("type", "Master of Science", "university", "University of Betelgeuse") + ), + "nationalities", List.of("British", "Betelgeusian") + ), + "cryptographic_holder_binding", true + ), null), List.of())); + + + @Test + @DisplayName("fails with no credentials") + void failsWithNoCredentials() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); + + assertFalse(queryResult.success()); + } + + @Test + @DisplayName("fails with credentials that do not satisfy a required claim_set") + void failsWithCredentialsThatDoNotSatisfyARequiredClaimSet() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); + + assertFalse(queryResult.success()); + } + + @Test + @DisplayName("return the requested sets") + void succeedsWithRequestedSets() throws JsonProcessingException { + List expectedIdCredentials = List.of(MDOC_MDL_ID); + List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + MDOC_MDL_ID, + MDOC_MDL_ADDRESS, + MDOC_PHOTO_CARD_ID, + MDOC_PHOTO_CARD_ADDRESS, + MDOC_EXAMPLE, + SD_JWT_VC_EXAMPLE)); + + assertTrue(queryResult.success()); + assertTrue(queryResult.credentials().containsKey("Identification")); + assertTrue(queryResult.credentials().containsKey("Proof of address")); + + List identification = queryResult.credentials().get("Identification"); + List poa = queryResult.credentials().get("Proof of address"); + + assertEquals(1, identification.size()); + assertEquals(1, poa.size()); + + expectedIdCredentials.forEach( + ec -> assertTrue(identification.contains(ec))); + expectedPoaCredentials.forEach( + ec -> assertTrue(poa.contains(ec))); + } + + @Test + @DisplayName("return alternative if not included") + void returnAlternative() throws JsonProcessingException { + List expectedIdCredentials = List.of(MDOC_PHOTO_CARD_ID); + List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); + + var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + MDOC_MDL_ADDRESS, + MDOC_PHOTO_CARD_ID, + MDOC_PHOTO_CARD_ADDRESS, + MDOC_EXAMPLE, + SD_JWT_VC_EXAMPLE)); + + assertTrue(queryResult.success()); + assertTrue(queryResult.credentials().containsKey("Identification")); + assertTrue(queryResult.credentials().containsKey("Proof of address")); + + List identification = queryResult.credentials().get("Identification"); + List poa = queryResult.credentials().get("Proof of address"); + + assertEquals(1, identification.size()); + assertEquals(1, poa.size()); + + expectedIdCredentials.forEach( + ec -> assertTrue(identification.contains(ec))); + expectedPoaCredentials.forEach( + ec -> assertTrue(poa.contains(ec))); + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java index 507d662..f261bcc 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java @@ -19,253 +19,253 @@ class DcqlQueryTest extends DcqlTest { - private static final String MDOC_MVRC_QUERY = "{\n" + - " \"credentials\": [\n" + - " {\n" + - " \"id\": \"my_credential\",\n" + - " \"format\": \"mso_mdoc\",\n" + - " \"meta\": { \"doctype_value\": \"org.iso.7367.1.mVRC\" },\n" + - " \"require_cryptographic_holder_binding\": true,\n" + - " \"claims\": [\n" + - " { \"path\": [\"org.iso.7367.1\", \"vehicle_holder\"], \"intent_to_retain\": false },\n" + - " { \"path\": [\"org.iso.18013.5.1\", \"first_name\"], \"intent_to_retain\": true }\n" + - " ],\n" + - " \"trusted_authorities\": [\n" + - " { \"type\": \"aki\", \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"] }\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - - private static final String MDOC_NAMESPACE_MVRC_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, - "claims": [ - { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, - { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } - ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), - "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") - ), - "authority", Map.of("type", "aki", "values", List.of("one")), - "cryptographic_holder_binding", true - ))); - - private static final Credential EXAMPLE_MDOC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "docType", "example_doctype", - "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), - "authority", Map.of("type", "aki", "values", List.of("something")), - "cryptographic_holder_binding", true - ))); - - private static final Credential EXAMPLE_SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( - new JwtCredential(null, - Map.of( - "vct", "https://credentials.example.com/identity_credential", - "_sd", List.of("hash-b", "hash-c"), - "address", Map.of("_sd", List.of("hash-a", "hash-x")), - "cryptographic_holder_binding", false), null), - List.of(new Disclosure("hash-a", "street_address", "42 Market Street"), - new Disclosure("hash-b", "first_name", "Arthur"), - new Disclosure("hash-c", "last_name", "Dent")) - )); - - private static final Credential EXAMPLE_W3C_LDP_VC = new Credential(CredentialFormat.LDP_VC, new LdpCredential(Map.of( - "type", List.of("https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"), - "credentialSubject", Map.of("first_name", "Arthur", "last_name", "Dent", "address", Map.of("street_address", "42 Market Street")), - "cryptographic_holder_binding", false - ))); - - - private static final String SD_JWT_VC_EXAMPLE_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "vc+sd-jwt", - "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, - "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final String SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "vc+sd-jwt", - "multiple": true, - "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, - "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final String SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "vc+sd-jwt", - "multiple": true, - "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, - "require_cryptographic_holder_binding": false - } - ] - } - """; - - private static final String W3C_LDP_VC_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "ldp_vc", - "meta": { - "type_values": [ - ["https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"], - ["https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#UniversityDegreeCredential"] - ] - }, - "claims": [ { "path": ["credentialSubject", "last_name"] }, { "path": ["credentialSubject", "first_name"] }, { "path": ["credentialSubject", "address", "street_address"] } ], - "require_cryptographic_holder_binding": false - } - ] - } - """; - - - @Test - @DisplayName("mdoc mvrc query fails with invalid mdoc") - void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); - - assertFalse(queryResult.success()); - } - - @Test - @DisplayName("mdoc mvrc example with multiple credentials succeeds") - void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - assertEquals(MDOC_MVRC, queryResult.credentials().get("credentials").get(0)); - } - - @Test - @DisplayName("w3cLdpVc example succeeds") - void w3cLdpVcExampleSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - assertEquals(EXAMPLE_W3C_LDP_VC, queryResult.credentials().get("credentials").get(0)); - } - - @Test - @DisplayName("w3cLdpVc query fails with invalid type values") - void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - - assertFalse(queryResult.success()); - } - - @Test - @DisplayName("mdocMvrc example using namespaces succeeds") - void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_NAMESPACE_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - } - - @Test - @DisplayName("sdJwtVc example with multiple credentials succeeds") - void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential theCredential = queryResult.credentials().get("credentials").get(0); - assertEquals(CredentialFormat.VC_SD_JWT, theCredential.getCredentialFormat()); - if (theCredential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(3, sdJwtCredential.getDisclosures().size()); - } else { - fail("It should be an SD-JWT credential."); - } - } - - @Test - @DisplayName("sdJwtVc with 'multiple' set to true succeeds") - void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); - assertTrue(queryResult.success()); - assertEquals(2, queryResult.credentials().get("credentials").size()); - } - - @Test - @DisplayName("sdJwtVc with 'multiple' set to true but only one credential in the presentation matches") - void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertEquals(3, sdJwtCredential.getDisclosures().size()); - } else { - fail("An SdJwtCredential should be contained."); - } - } - - @Test - @DisplayName("sdJwtVc with no claims should not disclose anything.") - void sdJwtVcWithNoClaims() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - Credential credential = queryResult.credentials().get("credentials").get(0); - assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); - if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { - assertTrue(sdJwtCredential.getDisclosures().isEmpty()); - } else { - fail("An SdJwtCredential should be contained."); - } - } + private static final String MDOC_MVRC_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"mso_mdoc\",\n" + + " \"meta\": { \"doctype_value\": \"org.iso.7367.1.mVRC\" },\n" + + " \"require_cryptographic_holder_binding\": true,\n" + + " \"claims\": [\n" + + " { \"path\": [\"org.iso.7367.1\", \"vehicle_holder\"], \"intent_to_retain\": false },\n" + + " { \"path\": [\"org.iso.18013.5.1\", \"first_name\"], \"intent_to_retain\": true }\n" + + " ],\n" + + " \"trusted_authorities\": [\n" + + " { \"type\": \"aki\", \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"] }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final String MDOC_NAMESPACE_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "authority", Map.of("type", "aki", "values", List.of("one")), + "cryptographic_holder_binding", true + ))); + + private static final Credential EXAMPLE_MDOC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "docType", "example_doctype", + "namespaces", Map.of("example_namespaces", Map.of("example_claim", "example_value")), + "authority", Map.of("type", "aki", "values", List.of("something")), + "cryptographic_holder_binding", true + ))); + + private static final Credential EXAMPLE_SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, + new SdJwtCredential( null, + new JwtCredential( null, null, + Map.of( + "vct", "https://credentials.example.com/identity_credential", + "_sd", List.of(getDisclosure("hash-b", "first_name", "Arthur").getSdHash(), getDisclosure("hash-c", "last_name", "Dent").getSdHash()), + "address", Map.of("_sd", List.of(getDisclosure("hash-a", "street_address", "42 Market Street").getSdHash(), "hash-x")), + "cryptographic_holder_binding", false), null), + List.of(getDisclosure("hash-a", "street_address", "42 Market Street"), + getDisclosure("hash-b", "first_name", "Arthur"), + getDisclosure("hash-c", "last_name", "Dent")) + )); + + private static final Credential EXAMPLE_W3C_LDP_VC = new Credential(CredentialFormat.LDP_VC, new LdpCredential( null, Map.of( + "type", List.of("https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"), + "credentialSubject", Map.of("first_name", "Arthur", "last_name", "Dent", "address", Map.of("street_address", "42 Market Street")), + "cryptographic_holder_binding", false + ))); + + + private static final String SD_JWT_VC_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "multiple": true, + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ { "path": ["last_name"] }, { "path": ["first_name"] }, { "path": ["address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "vc+sd-jwt", + "multiple": true, + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "require_cryptographic_holder_binding": false + } + ] + } + """; + + private static final String W3C_LDP_VC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "ldp_vc", + "meta": { + "type_values": [ + ["https://example.org/examples#AlumniCredential", "https://example.org/examples#BachelorDegree"], + ["https://www.w3.org/2018/credentials#VerifiableCredential", "https://example.org/examples#UniversityDegreeCredential"] + ] + }, + "claims": [ { "path": ["credentialSubject", "last_name"] }, { "path": ["credentialSubject", "first_name"] }, { "path": ["credentialSubject", "address", "street_address"] } ], + "require_cryptographic_holder_binding": false + } + ] + } + """; + + + @Test + @DisplayName("mdoc mvrc query fails with invalid mdoc") + void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); + + assertFalse(queryResult.success()); + } + + @Test + @DisplayName("mdoc mvrc example with multiple credentials succeeds") + void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(MDOC_MVRC, queryResult.credentials().get("credentials").get(0)); + } + + @Test + @DisplayName("w3cLdpVc example succeeds") + void w3cLdpVcExampleSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(EXAMPLE_W3C_LDP_VC, queryResult.credentials().get("credentials").get(0)); + } + + @Test + @DisplayName("w3cLdpVc query fails with invalid type values") + void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertFalse(queryResult.success()); + } + + @Test + @DisplayName("mdocMvrc example using namespaces succeeds") + void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_NAMESPACE_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + } + + @Test + @DisplayName("sdJwtVc example with multiple credentials succeeds") + void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential theCredential = queryResult.credentials().get("credentials").get(0); + assertEquals(CredentialFormat.VC_SD_JWT, theCredential.getCredentialFormat()); + if (theCredential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(3, sdJwtCredential.getDisclosures().size()); + } else { + fail("It should be an SD-JWT credential."); + } + } + + @Test + @DisplayName("sdJwtVc with 'multiple' set to true succeeds") + void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); + assertTrue(queryResult.success()); + assertEquals(2, queryResult.credentials().get("credentials").size()); + } + + @Test + @DisplayName("sdJwtVc with 'multiple' set to true but only one credential in the presentation matches") + void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertEquals(3, sdJwtCredential.getDisclosures().size()); + } else { + fail("An SdJwtCredential should be contained."); + } + } + + @Test + @DisplayName("sdJwtVc with no claims should not disclose anything.") + void sdJwtVcWithNoClaims() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + Credential credential = queryResult.credentials().get("credentials").get(0); + assertEquals(CredentialFormat.VC_SD_JWT, credential.getCredentialFormat()); + if (credential.getRawCredential() instanceof SdJwtCredential sdJwtCredential) { + assertTrue(sdJwtCredential.getDisclosures().isEmpty()); + } else { + fail("An SdJwtCredential should be contained."); + } + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java index f8411a6..5cc5232 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java @@ -24,119 +24,119 @@ class DcqlQueryTrustedAuthoritiesTest extends DcqlTest { - // --- Test Data --- - - private static final Map ETSI_TL_AUTHORITY = Map.of("type", "etsi_tl", "values", List.of("https://list.com")); - private static final Map OPENID_FEDERATION_AUTHORITY = Map.of("type", "openid_federation", "values", List.of("https://federation.com")); - - - private static final String MDOC_MVRC_QUERY = "{\n" + - " \"credentials\": [\n" + - " {\n" + - " \"id\": \"my_credential\",\n" + - " \"format\": \"mso_mdoc\",\n" + - " \"trusted_authorities\": [\n" + - " {\n" + - " \"type\": \"aki\",\n" + - " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\", \"UVVJUkVELiBBIHN0cmluZyB1bmlxdWVseSBpZGVudGlmeWluZyB0aGUgdHlwZSA\"]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - - private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( - "credential_format", "mso_mdoc", - "doctype", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), - "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") - ), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MVRC_ALT_AKI = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of(generateTestCertificate(generateTestKeyPair()))), Map.of( - "credential_format", "mso_mdoc", - "doctype", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), - "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") - ), - "cryptographic_holder_binding", true - ))); - - private static final Credential MDOC_MVRC_NO_X5C = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(new MDocHeaders(null, List.of()), Map.of( - "credential_format", "mso_mdoc", - "doctype", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), - "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") - ), - "cryptographic_holder_binding", true - ))); - - private static final String SD_JWT_VC_EXAMPLE_QUERY = "{\n" + - " \"credentials\": [\n" + - " {\n" + - " \"id\": \"my_credential\",\n" + - " \"format\": \"vc+sd-jwt\",\n" + - " \"trusted_authorities\": [\n" + - " {\n" + - " \"type\": \"aki\",\n" + - " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"]\n" + - " }\n" + - " ]\n" + - " }\n" + - " ]\n" + - "}"; - - private static final Credential SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential(new JwtCredential(Map.of("x5c", List.of(generateTestCertificate(TEST_KEY))), Map.of( - "credential_format", "vc+sd-jwt", - "vct", "https://credentials.example.com/identity_credential", - "claims", Map.of("first_name", "Arthur", "last_name", "Dent"), - "cryptographic_holder_binding", true - ), null), List.of())); - - - @Test - @DisplayName("mdocMvrc example with trusted_authorities succeeds") - void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); - - assertTrue(credentialsResult.success()); - assertEquals(1, credentialsResult.credentials().get("credentials").size()); - } - - @Test - @DisplayName("mdocMvrc example where authority does not match trusted_authorities entry") - void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); - - assertFalse(credentialsResult.success()); - } - - @Test - @DisplayName("mdocMvrc example where trusted_authorities is present but no authority") - void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); - - assertFalse(credentialsResult.success()); - } - - @Test - @DisplayName("sdJwtVc example with trusted_authorities succeeds") - void sdJwtVcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { - - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); - - assertTrue(credentialsResult.success()); - assertEquals(1, credentialsResult.credentials().get("credentials").size()); - } + // --- Test Data --- + + private static final Map ETSI_TL_AUTHORITY = Map.of("type", "etsi_tl", "values", List.of("https://list.com")); + private static final Map OPENID_FEDERATION_AUTHORITY = Map.of("type", "openid_federation", "values", List.of("https://federation.com")); + + + private static final String MDOC_MVRC_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"mso_mdoc\",\n" + + " \"trusted_authorities\": [\n" + + " {\n" + + " \"type\": \"aki\",\n" + + " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\", \"UVVJUkVELiBBIHN0cmluZyB1bmlxdWVseSBpZGVudGlmeWluZyB0aGUgdHlwZSA\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final Credential MDOC_MVRC = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_ALT_AKI = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(generateTestKeyPair()))), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential MDOC_MVRC_NO_X5C = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of()), Map.of( + "credential_format", "mso_mdoc", + "doctype", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", "Martin Auer") + ), + "cryptographic_holder_binding", true + ))); + + private static final String SD_JWT_VC_EXAMPLE_QUERY = "{\n" + + " \"credentials\": [\n" + + " {\n" + + " \"id\": \"my_credential\",\n" + + " \"format\": \"vc+sd-jwt\",\n" + + " \"trusted_authorities\": [\n" + + " {\n" + + " \"type\": \"aki\",\n" + + " \"values\": [\"" + Base64.getUrlEncoder().encodeToString(generateTestAki(TEST_KEY).getKeyIdentifier()) + "\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + private static final Credential SD_JWT_VC = new Credential(CredentialFormat.VC_SD_JWT, new SdJwtCredential( null, new JwtCredential( null, Map.of("x5c", List.of(generateTestCertificate(TEST_KEY))), Map.of( + "credential_format", "vc+sd-jwt", + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of("first_name", "Arthur", "last_name", "Dent"), + "cryptographic_holder_binding", true + ), null), List.of())); + + + @Test + @DisplayName("mdocMvrc example with trusted_authorities succeeds") + void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + + assertTrue(credentialsResult.success()); + assertEquals(1, credentialsResult.credentials().get("credentials").size()); + } + + @Test + @DisplayName("mdocMvrc example where authority does not match trusted_authorities entry") + void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); + + assertFalse(credentialsResult.success()); + } + + @Test + @DisplayName("mdocMvrc example where trusted_authorities is present but no authority") + void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); + + assertFalse(credentialsResult.success()); + } + + @Test + @DisplayName("sdJwtVc example with trusted_authorities succeeds") + void sdJwtVcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { + + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); + + assertTrue(credentialsResult.success()); + assertEquals(1, credentialsResult.credentials().get("credentials").size()); + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java index 24c2080..0fd829b 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java @@ -23,109 +23,109 @@ class DcqlQueryWithJsonTransformTest extends DcqlTest { - /** - * A placeholder for a class that might be used in credential data and needs special handling, - * similar to the ValueClass in the TypeScript test. - */ - static class ValueClass { - private final Object value; - - public ValueClass(Object value) { - this.value = value; - } - - public Object toJson() { - return this.value; - } - - @Override - public boolean equals(Object o) { - return o instanceof ValueClass && ((ValueClass) o).value.equals(this.value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - } - - // --- Test Data --- - - private static final String MDOC_MVRC_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "mso_mdoc", - "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, - "claims": [ - { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, - { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } - ] - } - ] - } - """; - - private static final String SD_JWT_VC_EXAMPLE_QUERY = """ - { - "credentials": [ - { - "id": "my_credential", - "format": "dc+sd-jwt", - "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, - "claims": [ - { "path": ["last_name"] }, - { "path": ["first_name"] }, - { "path": ["address", "street_address"] }, - { "path": ["org.iso.7367.1", "vehicle_holder"], "values": ["Timo Glastra"] } - ] - } - ] - } - """; - - private static final Credential MDOC_WITH_JT = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, Map.of( - "docType", "org.iso.7367.1.mVRC", - "namespaces", Map.of( - "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), - "org.iso.18013.5.1", Map.of("first_name", new ValueClass("Martin Auer")) - ), - "cryptographic_holder_binding", true - ))); - - private static final Credential SD_JWT_VC_WITH_JT = new Credential(CredentialFormat.DC_SD_JWT, - new SdJwtCredential( - new JwtCredential(null, Map.of( - "vct", "https://credentials.example.com/identity_credential", - "claims", Map.of( - "first_name", "Arthur", - "last_name", "Dent", - "address", Map.of("street_address", new ValueClass("42 Market Street"), "locality", "Milliways", "postal_code", "12345"), - "org.iso.7367.1", Map.of("vehicle_holder", "Timo Glastra") - ), - "cryptographic_holder_binding", true - ), null), List.of())); - - @Test - @DisplayName("mdocMvrc example succeeds") - void mdocMvrcExampleSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - } - - @Test - @DisplayName("sdJwtVc example with multiple credentials succeeds") - void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { - var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); - - assertTrue(queryResult.success()); - assertEquals(1, queryResult.credentials().get("credentials").size()); - assertEquals(CredentialFormat.DC_SD_JWT, queryResult.credentials().get("credentials").get(0).getCredentialFormat()); - } + /** + * A placeholder for a class that might be used in credential data and needs special handling, + * similar to the ValueClass in the TypeScript test. + */ + static class ValueClass { + private final Object value; + + public ValueClass(Object value) { + this.value = value; + } + + public Object toJson() { + return this.value; + } + + @Override + public boolean equals(Object o) { + return o instanceof ValueClass && ((ValueClass) o).value.equals(this.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + } + + // --- Test Data --- + + private static final String MDOC_MVRC_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "mso_mdoc", + "meta": { "doctype_value": "org.iso.7367.1.mVRC" }, + "claims": [ + { "namespace": "org.iso.7367.1", "claim_name": "vehicle_holder" }, + { "namespace": "org.iso.18013.5.1", "claim_name": "first_name" } + ] + } + ] + } + """; + + private static final String SD_JWT_VC_EXAMPLE_QUERY = """ + { + "credentials": [ + { + "id": "my_credential", + "format": "dc+sd-jwt", + "meta": { "vct_values": ["https://credentials.example.com/identity_credential"] }, + "claims": [ + { "path": ["last_name"] }, + { "path": ["first_name"] }, + { "path": ["address", "street_address"] }, + { "path": ["org.iso.7367.1", "vehicle_holder"], "values": ["Timo Glastra"] } + ] + } + ] + } + """; + + private static final Credential MDOC_WITH_JT = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, null, Map.of( + "docType", "org.iso.7367.1.mVRC", + "namespaces", Map.of( + "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer", "non_disclosed", "secret"), + "org.iso.18013.5.1", Map.of("first_name", new ValueClass("Martin Auer")) + ), + "cryptographic_holder_binding", true + ))); + + private static final Credential SD_JWT_VC_WITH_JT = new Credential(CredentialFormat.DC_SD_JWT, + new SdJwtCredential( null, + new JwtCredential( null, null, Map.of( + "vct", "https://credentials.example.com/identity_credential", + "claims", Map.of( + "first_name", "Arthur", + "last_name", "Dent", + "address", Map.of("street_address", new ValueClass("42 Market Street"), "locality", "Milliways", "postal_code", "12345"), + "org.iso.7367.1", Map.of("vehicle_holder", "Timo Glastra") + ), + "cryptographic_holder_binding", true + ), null), List.of())); + + @Test + @DisplayName("mdocMvrc example succeeds") + void mdocMvrcExampleSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + } + + @Test + @DisplayName("sdJwtVc example with multiple credentials succeeds") + void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { + var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); + QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); + + assertTrue(queryResult.success()); + assertEquals(1, queryResult.credentials().get("credentials").size()); + assertEquals(CredentialFormat.DC_SD_JWT, queryResult.credentials().get("credentials").get(0).getCredentialFormat()); + } } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java index c0fbc66..e125699 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java @@ -1,5 +1,6 @@ package io.github.wistefan.dcql.query; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; @@ -8,6 +9,7 @@ import io.github.wistefan.dcql.helper.TrustedAuthorityTypeDeserializer; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.TrustedAuthorityType; +import io.github.wistefan.dcql.model.credential.Disclosure; import org.bouncycastle.asn1.x509.*; import org.bouncycastle.cert.X509ExtensionUtils; import org.bouncycastle.cert.X509v3CertificateBuilder; @@ -24,93 +26,105 @@ import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.cert.X509Certificate; +import java.util.Base64; import java.util.Date; +import java.util.List; public abstract class DcqlTest { - public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - { - OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - SimpleModule deserializerModule = new SimpleModule(); - deserializerModule.addDeserializer(CredentialFormat.class, new CredentialFormatDeserializer()); - deserializerModule.addDeserializer(TrustedAuthorityType.class, new TrustedAuthorityTypeDeserializer()); - OBJECT_MAPPER.registerModule(deserializerModule); - } - - public static final KeyPair TEST_KEY = generateTestKeyPair(); - - - public static KeyPair generateTestKeyPair() { - try { - // Generate keypair - KeyPairGenerator keyGen = null; - - keyGen = KeyPairGenerator.getInstance("RSA"); - - keyGen.initialize(2048); - return keyGen.generateKeyPair(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - public static AuthorityKeyIdentifier generateTestAki(KeyPair testKey) { - - Security.addProvider(new BouncyCastleProvider()); - try { - X509ExtensionUtils extUtils = new X509ExtensionUtils( - new JcaDigestCalculatorProviderBuilder() - .setProvider("BC") - .build() - .get(new AlgorithmIdentifier(X509ObjectIdentifiers.id_SHA1)) - ); - - // Now you can create SKI and AKI - SubjectPublicKeyInfo subjectPublicKeyInfo = - SubjectPublicKeyInfo.getInstance(testKey.getPublic().getEncoded()); - - return extUtils.createAuthorityKeyIdentifier(subjectPublicKeyInfo); - - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - - - public static X509Certificate generateTestCertificate(KeyPair testKey) { - - Security.addProvider(new BouncyCastleProvider()); - try { - Security.addProvider(new BouncyCastleProvider()); - // Certificate details - String issuer = "CN=Test CA"; - String subject = "CN=Test Cert"; - BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); - Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60); - Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365)); - - // Builder - X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( - new javax.security.auth.x500.X500Principal(issuer), - serial, - notBefore, - notAfter, - new javax.security.auth.x500.X500Principal(subject), - testKey.getPublic() - ); - certBuilder.addExtension(Extension.authorityKeyIdentifier, false, generateTestAki(testKey)); - ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") - .build(testKey.getPrivate()); - X509Certificate cert = new JcaX509CertificateConverter() - .setProvider("BC") - .getCertificate(certBuilder.build(signer)); - return cert; - - } catch (Exception e) { - throw new RuntimeException(e); - } - } + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + { + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + SimpleModule deserializerModule = new SimpleModule(); + deserializerModule.addDeserializer(CredentialFormat.class, new CredentialFormatDeserializer()); + deserializerModule.addDeserializer(TrustedAuthorityType.class, new TrustedAuthorityTypeDeserializer()); + OBJECT_MAPPER.registerModule(deserializerModule); + } + + public static final KeyPair TEST_KEY = generateTestKeyPair(); + + + public static KeyPair generateTestKeyPair() { + try { + // Generate keypair + KeyPairGenerator keyGen = null; + + keyGen = KeyPairGenerator.getInstance("RSA"); + + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static AuthorityKeyIdentifier generateTestAki(KeyPair testKey) { + + Security.addProvider(new BouncyCastleProvider()); + try { + X509ExtensionUtils extUtils = new X509ExtensionUtils( + new JcaDigestCalculatorProviderBuilder() + .setProvider("BC") + .build() + .get(new AlgorithmIdentifier(X509ObjectIdentifiers.id_SHA1)) + ); + + // Now you can create SKI and AKI + SubjectPublicKeyInfo subjectPublicKeyInfo = + SubjectPublicKeyInfo.getInstance(testKey.getPublic().getEncoded()); + + return extUtils.createAuthorityKeyIdentifier(subjectPublicKeyInfo); + + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + + public static X509Certificate generateTestCertificate(KeyPair testKey) { + + Security.addProvider(new BouncyCastleProvider()); + try { + Security.addProvider(new BouncyCastleProvider()); + // Certificate details + String issuer = "CN=Test CA"; + String subject = "CN=Test Cert"; + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + Date notBefore = new Date(System.currentTimeMillis() - 1000L * 60 * 60); + Date notAfter = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365)); + + // Builder + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + new javax.security.auth.x500.X500Principal(issuer), + serial, + notBefore, + notAfter, + new javax.security.auth.x500.X500Principal(subject), + testKey.getPublic() + ); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, generateTestAki(testKey)); + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .build(testKey.getPrivate()); + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certBuilder.build(signer)); + return cert; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static Disclosure getDisclosure(String salt, String claim, Object value) { + try { + byte[] encoded = new ObjectMapper().writeValueAsBytes(List.of(salt, claim, value)); + return new Disclosure(salt, claim, value, Base64.getUrlEncoder().encodeToString(encoded), "sha-256"); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } From ce32f79c9bed6e6acb443dd70bdb188536972822 Mon Sep 17 00:00:00 2001 From: Stefan Wiedemann Date: Wed, 1 Oct 2025 15:14:43 +0200 Subject: [PATCH 8/8] doc and updates --- README.md | 69 +++++ pom.xml | 7 + .../github/wistefan/dcql/ClaimsEvaluator.java | 37 ++- .../wistefan/dcql/CredentialEvaluator.java | 30 +++ .../wistefan/dcql/CredentialMapper.java | 22 ++ .../github/wistefan/dcql/DCQLEvaluator.java | 241 ++---------------- .../dcql/DcSdJwtCredentialEvaluator.java | 11 + .../wistefan/dcql/JwtCredentialEvaluator.java | 67 +++++ .../wistefan/dcql/LdpCredentialEvaluator.java | 65 +++++ .../dcql/MDocCredentialEvaluator.java | 91 +++++++ .../dcql/SdJwtCredentialEvaluator.java | 99 +++++++ .../dcql/VcSdJwtCredentialEvaluator.java | 10 + .../wistefan/dcql/model/ClaimsQuery.java | 3 + .../wistefan/dcql/model/Credential.java | 3 + .../wistefan/dcql/model/CredentialFormat.java | 3 + .../wistefan/dcql/model/CredentialQuery.java | 1 + .../dcql/model/CredentialSetQuery.java | 1 + .../github/wistefan/dcql/model/DcqlQuery.java | 1 + .../wistefan/dcql/model/JwtMetaData.java | 36 +-- .../wistefan/dcql/model/MDocMetaData.java | 6 + .../dcql/model/TrustedAuthorityQuery.java | 1 + .../dcql/model/TrustedAuthorityType.java | 3 + .../wistefan/dcql/model/W3CMetaData.java | 7 +- .../dcql/model/credential/CredentialBase.java | 3 + .../dcql/model/credential/Disclosure.java | 7 + .../dcql/model/credential/JwtCredential.java | 24 +- .../dcql/model/credential/LdpCredential.java | 6 + .../dcql/model/credential/MDocCredential.java | 6 + .../dcql/model/credential/MDocHeaders.java | 3 + .../model/credential/SdJwtCredential.java | 18 +- .../dcql/example/ParseCredentialTest.java | 94 +++++++ .../dcql/query/DcqlClaimSetQueryTest.java | 48 ++-- .../dcql/query/DcqlQueryComplexTest.java | 9 +- .../wistefan/dcql/query/DcqlQueryTest.java | 19 +- .../DcqlQueryTrustedAuthoritiesTest.java | 10 +- .../query/DcqlQueryWithJsonTransformTest.java | 6 +- .../github/wistefan/dcql/query/DcqlTest.java | 13 + src/test/resources/example/legalPerson.sd_jwt | 1 + src/test/resources/example/userCredential.jwt | 1 + 39 files changed, 772 insertions(+), 310 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/io/github/wistefan/dcql/CredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/DcSdJwtCredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/JwtCredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/LdpCredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/MDocCredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/SdJwtCredentialEvaluator.java create mode 100644 src/main/java/io/github/wistefan/dcql/VcSdJwtCredentialEvaluator.java create mode 100644 src/test/java/io/github/wistefan/dcql/example/ParseCredentialTest.java create mode 100644 src/test/resources/example/legalPerson.sd_jwt create mode 100644 src/test/resources/example/userCredential.jwt 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 index be42985..9791971 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ 5.13.4 2.20.0 + 10.5 @@ -148,6 +149,12 @@ ${version.org.junit.jupiter} test + + com.nimbusds + nimbus-jose-jwt + ${version.com.nimbusds.nimbus-jose-jwt} + test + diff --git a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java index af6d0a2..8d48bdd 100644 --- a/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/ClaimsEvaluator.java @@ -8,12 +8,18 @@ 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 { @@ -34,6 +40,10 @@ public static Optional evaluateClaimsForMDocCredential(ClaimsQue 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 { @@ -63,6 +73,9 @@ private static SdJwtCredential cleanUpDisclosures(List selectedCl 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 { @@ -82,6 +95,9 @@ public static Optional evaluateClaimsForJwtCredential(ClaimsQuery return Optional.empty(); } + /** + * Evaluate the claims query for LDP Credentials + */ public static Optional evaluateClaimsForLdpCredential(ClaimsQuery claimsQuery, LdpCredential credential) { List selectedClaims = new ArrayList<>(); try { @@ -102,20 +118,19 @@ public static Optional evaluateClaimsForLdpCredential(ClaimsQuery } - public static List selectClaimsByPath(Map credential, List claimPath) { + private static List selectClaimsByPath(Map credential, List claimPath) { return processPath(credential, claimPath, null); } - public static List selectClaimsByPathDisclosures(Map credential, List claimPath, - List disclosures) { + private static List selectClaimsByPathDisclosures(Map credential, List claimPath, + List disclosures) { return processPath(credential, claimPath, disclosures); } private static List processPath( Map credential, List claimPath, - List disclosures - ) { + List disclosures) { if (credential == null || claimPath == null || claimPath.isEmpty()) { throw new IllegalArgumentException("Credential and claimPath must not be null or empty"); } @@ -134,11 +149,7 @@ private static List processPath( if (disclosures != null && candidate instanceof Map mapCandidate && mapCandidate.containsKey(SD_KEY)) { Object sdObj = mapCandidate.get(SD_KEY); Map revealed = null; - try { - revealed = getStringSelectedClaimMap(disclosures, sdObj); - } catch (NoSuchAlgorithmException e) { - throw new EvaluationException("Was not able to reveal selective disclosure.", e); - } + revealed = getStringSelectedClaimMap(disclosures, sdObj); // Merge: start with revealed, then copy original entries (except "_sd"), // so explicit values in the original map overwrite revealed ones if keys collide. @@ -192,19 +203,16 @@ private static List processPath( throw new IllegalArgumentException("Invalid claim path component: " + component); } } - if (nextSelection.isEmpty()) { throw new IllegalArgumentException("No elements selected at path component: " + component); } - current = nextSelection; } - return current; } - private static Map getStringSelectedClaimMap(List disclosures, Object sdObj) throws NoSuchAlgorithmException { + private static Map getStringSelectedClaimMap(List disclosures, Object sdObj) { if (!(sdObj instanceof List sdList)) { throw new IllegalArgumentException("_sd field must be a list"); } @@ -222,6 +230,7 @@ private static Map getStringSelectedClaimMap(List { + + /** + * Returns the {@link CredentialFormat} suppored by that evaluator. + */ + CredentialFormat supportedFormat(); + + /** + * Translates the list of {@link Credential}s into the concrete types supported by that evaluator. Will fail if + * the list contains other types. + */ + List translate(List credentials); + + /** + * Evaluate the query on the list of credentials. + */ + List evaluate(CredentialQuery credentialQuery, List credentialsList); +} diff --git a/src/main/java/io/github/wistefan/dcql/CredentialMapper.java b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java index 66f3669..c34ead1 100644 --- a/src/main/java/io/github/wistefan/dcql/CredentialMapper.java +++ b/src/main/java/io/github/wistefan/dcql/CredentialMapper.java @@ -7,8 +7,18 @@ import java.util.ArrayList; import java.util.List; +/** + * Helper class to map raw {@link Credential}s into concrete Credential-Classes. + */ public class CredentialMapper { + /** + * Convert the list of raw credentials into typed credentials. + * + * @param credentialFormat format of the credentials in the list + * @param rawCredentials raw credentials to be mapped + * @return the list of typed credentials + */ public static List toCredentials(CredentialFormat credentialFormat, List rawCredentials) { return rawCredentials.stream() .filter(CredentialBase.class::isInstance) @@ -17,6 +27,9 @@ public static List toCredentials(CredentialFormat credentialFormat, .toList(); } + /** + * Return the {@link LdpCredential}s from the given list. Fails if the list is multi-credential. + */ public static List toLdpCredentials(List credentialsList) { List ldpCredentialsList = new ArrayList<>(); for (Credential c : credentialsList) { @@ -29,6 +42,9 @@ public static List toLdpCredentials(List credentialsL return ldpCredentialsList; } + /** + * Return the {@link MDocCredential}s from the given list. Fails if the list is multi-credential. + */ public static List toMDocCredentials(List credentialsList) { List mDocCredentialsList = new ArrayList<>(); for (Credential c : credentialsList) { @@ -41,6 +57,9 @@ public static List toMDocCredentials(List credential return mDocCredentialsList; } + /** + * Return the {@link JwtCredential}s from the given list. Fails if the list is multi-credential. + */ public static List toJWTCredentials(List credentialsList) { List jwtCredentialsList = new ArrayList<>(); for (Credential c : credentialsList) { @@ -53,6 +72,9 @@ public static List toJWTCredentials(List credentialsL return jwtCredentialsList; } + /** + * Return the {@link SdJwtCredential}s from the given list. Fails if the list is multi-credential. + */ public static List toSdJWTCredentials(List credentialsList) { List sdJwtCredentialsList = new ArrayList<>(); for (Credential c : credentialsList) { diff --git a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java index 403eb5f..114828d 100644 --- a/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java +++ b/src/main/java/io/github/wistefan/dcql/DCQLEvaluator.java @@ -2,20 +2,26 @@ import io.github.wistefan.dcql.model.*; import io.github.wistefan.dcql.model.credential.*; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import java.util.*; import java.util.function.BiFunction; import java.util.stream.Collectors; +/** + * Evaluator for DCQL Queries{@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l} + */ +@RequiredArgsConstructor @Slf4j public class DCQLEvaluator { + // default key for non-credential-set results. private static final String DEFAULT_KEY = "credentials"; - private static final String MDOC_NAMESPACE_KEY = "namespaces"; + private final List credentialEvaluators; - public static QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { + public QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List credentialsList) { if (containsCredentialSets(dcqlQuery)) { // linked map to contain set order Map> resultMap = new LinkedHashMap<>(); @@ -52,9 +58,9 @@ public static QueryResult evaluateDCQLQuery(DcqlQuery dcqlQuery, List evaluateCredentialSetQuery(Map credentialQueryMap, - CredentialSetQuery credentialSetQuery, - List credentials) { + private List evaluateCredentialSetQuery(Map credentialQueryMap, + CredentialSetQuery credentialSetQuery, + List credentials) { for (List option : credentialSetQuery.getOptions()) { // set to prevent duplicates Set fullfillingCredentials = new HashSet<>(); @@ -72,7 +78,7 @@ private static List evaluateCredentialSetQuery(Map evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { + private List evaluateCredentialQuery(CredentialQuery credentialQuery, List credentialsList) { if (!containsClaims(credentialQuery) && containsClaimSets(credentialQuery)) { @@ -80,132 +86,17 @@ && containsClaimSets(credentialQuery)) { } List filteredByFormat = filterByFormat(credentialQuery.getFormat(), credentialsList); - return switch (credentialQuery.getFormat()) { - case LDP_VC -> evaluateForLdpVC(credentialQuery, filteredByFormat); - case MSO_MDOC -> evaluateForMDoc(credentialQuery, filteredByFormat); - case DC_SD_JWT, VC_SD_JWT -> evaluateForSdJwt(credentialQuery, filteredByFormat); - case JWT_VC_JSON -> evaluateForJwt(credentialQuery, filteredByFormat); - }; - } - - private static List evaluateForSdJwt(CredentialQuery credentialQuery, List credentialsList) { - List sdJwtCredentials = CredentialMapper.toSdJWTCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - sdJwtCredentials = filterSdJwtByMetadata(credentialQuery.getMeta(), sdJwtCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - sdJwtCredentials = sdJwtCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForSDJwtCredential(taq, credential)) - .toList(); - } - } - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - sdJwtCredentials = evaluateSdJwtCredentialsQuery(credentialQuery, sdJwtCredentials); - } else if (containsClaims(credentialQuery)) { - return evaluateSdJwtForClaimSet(credentialQuery, sdJwtCredentials); - } else { - sdJwtCredentials = sdJwtCredentials.stream() - // keep the original credential untouched - .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getRaw(), sdJwtCredential.getJwtCredential(), List.of())) - .toList(); - } - return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); - } - - private static List evaluateForJwt(CredentialQuery credentialQuery, List credentialsList) { - List jwtCredentials = CredentialMapper.toJWTCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - jwtCredentials = filterJwtByMetadata(credentialQuery.getMeta(), jwtCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - jwtCredentials = jwtCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForJwtCredential(taq, credential)) - .toList(); - } - } - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - jwtCredentials = evaluateJwtCredentialsClaimQuery(cq, jwtCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, jwtCredentials, DCQLEvaluator::evaluateJwtCredentialsClaimQuery); - } - return CredentialMapper.toCredentials(CredentialFormat.JWT_VC_JSON, jwtCredentials); - } - - private static List evaluateForLdpVC(CredentialQuery credentialQuery, List credentialsList) { - List ldpCredentials = CredentialMapper.toLdpCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - ldpCredentials = filterLdpByMetadata(credentialQuery.getMeta(), ldpCredentials); - } - - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - ldpCredentials = evaluateLdpCredentialsClaimQuery(cq, ldpCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, ldpCredentials, DCQLEvaluator::evaluateLdpCredentialsClaimQuery); - } - - return CredentialMapper.toCredentials(CredentialFormat.LDP_VC, ldpCredentials); - } - - private static List evaluateForMDoc(CredentialQuery credentialQuery, List credentialsList) { - List mDocCredentials = CredentialMapper.toMDocCredentials(credentialsList); - if (containsMeta(credentialQuery)) { - mDocCredentials = filterMDocByMetadata(credentialQuery.getMeta(), mDocCredentials); - } - if (containsTrustAuthorities(credentialQuery)) { - for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { - mDocCredentials = mDocCredentials.stream() - .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForMDocCredential(taq, credential)) - .toList(); - } - } - translateMDocQueries(credentialQuery); - if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { - for (ClaimsQuery cq : credentialQuery.getClaims()) { - mDocCredentials = evaluateMDocCredentialsClaimQuery(cq, mDocCredentials); - } - } else if (containsClaims(credentialQuery)) { - return evaluateForClaimSet(credentialQuery, mDocCredentials, DCQLEvaluator::evaluateMDocCredentialsClaimQuery); - } - return CredentialMapper.toCredentials(CredentialFormat.MSO_MDOC, mDocCredentials); - } - - private static List evaluateSdJwtForClaimSet(CredentialQuery credentialQuery, List sdJwtCredentials) { - Map claimsQueryMap = new HashMap<>(); - credentialQuery.getClaims() - .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); - - for (List claimSet : credentialQuery.getClaimSets()) { - List disclosedCredentials = new ArrayList<>(); - for (SdJwtCredential credential : sdJwtCredentials) { - Set disclosures = new HashSet<>(); - for (String claimId : claimSet) { - ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); - disclosures.addAll(new HashSet<>( - ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, credential) - .map(SdJwtCredential::getDisclosures) - .orElse(new ArrayList<>()))); - } - if (!disclosures.isEmpty()) { - disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(disclosures))); - } - } - - if (!disclosedCredentials.isEmpty()) { - return CredentialMapper.toCredentials(credentialQuery.getFormat(), disclosedCredentials); - } - } - return List.of(); + CredentialEvaluator credentialEvaluator = this.credentialEvaluators + .stream() + .filter(evaluator -> evaluator.supportedFormat() == credentialQuery.getFormat()) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("The format %s is not supported. Consider registering a matching evaluator.", credentialQuery.getFormat()))); + return credentialEvaluator.evaluate(credentialQuery, credentialEvaluator.translate(filteredByFormat)); } // The method returns the first claim set that is fullfilled. It can contain multiple credentials, that would // fulfill the set individually, leaving the choice of what to share to the upstream. - private static List evaluateForClaimSet(CredentialQuery credentialQuery, List initialCredentials, BiFunction, List> evaluationFunction) { + protected static List evaluateForClaimSet(CredentialQuery credentialQuery, List initialCredentials, BiFunction, List> evaluationFunction) { Map claimsQueryMap = new HashMap<>(); credentialQuery.getClaims() .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); @@ -223,67 +114,6 @@ private static List evaluateForClaimSet(CredentialQuery credenti return List.of(); } - private static List evaluateSdJwtCredentialsQuery(CredentialQuery credentialQuery, List sdJwtCredentials) { - List disclosedCredentials = new ArrayList<>(); - for (SdJwtCredential credential : sdJwtCredentials) { - Set selectedDisclosures = credentialQuery.getClaims() - .stream() - .map(cq -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .map(SdJwtCredential::getDisclosures) - .flatMap(List::stream) - .collect(Collectors.toSet()); - disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); - } - return disclosedCredentials; - } - - private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { - return ldpCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static List evaluateJwtCredentialsClaimQuery(ClaimsQuery cq, List jwtCredentials) { - return jwtCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForJwtCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static List evaluateMDocCredentialsClaimQuery(ClaimsQuery cq, List mDocCredentials) { - return mDocCredentials.stream() - .map(credential -> ClaimsEvaluator.evaluateClaimsForMDocCredential(cq, credential)) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - } - - private static boolean isMDocClaimsQuery(ClaimsQuery claimsQuery) { - if ((claimsQuery.getNamespace() != null && claimsQuery.getClaimName() == null) || (claimsQuery.getNamespace() == null && claimsQuery.getClaimName() != null)) { - throw new IllegalArgumentException("When a namespace or claim_name is set, the other parameter is mandatory."); - } - return claimsQuery.getIntent_to_retain() != null || claimsQuery.getNamespace() != null; - } - - private static CredentialQuery translateMDocQueries(CredentialQuery credentialQuery) { - if (credentialQuery.getClaims() == null) { - return credentialQuery; - } - credentialQuery.getClaims() - .forEach(cq -> { - if (isMDocClaimsQuery(cq) && cq.getNamespace() != null) { - cq.setPath(List.of(MDOC_NAMESPACE_KEY, cq.getNamespace(), cq.getClaimName())); - } else { - cq.getPath().addFirst(MDOC_NAMESPACE_KEY); - } - }); - return credentialQuery; - } private static List filterByFormat(CredentialFormat credentialFormat, List credentialsList) { return credentialsList.stream() @@ -291,39 +121,6 @@ private static List filterByFormat(CredentialFormat credentialFormat .toList(); } - private static List filterLdpByMetadata(Map metaData, List credentialsList) { - W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(ldpCredential -> - w3CMetaData.getTypeValues() - .stream() - .anyMatch(metaTypes -> new HashSet<>(ldpCredential.getType()).containsAll(metaTypes))) - .toList(); - } - - private static List filterSdJwtByMetadata(Map metaData, List credentialsList) { - JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(sdJwtCredential -> jwtMetaData.getVctValues().contains(sdJwtCredential.getVct())) - .toList(); - } - - private static List filterJwtByMetadata(Map metaData, List credentialsList) { - W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(jwtCredential -> - w3CMetaData.getTypeValues() - .stream() - .anyMatch(metaTypes -> new HashSet<>(jwtCredential.getType()).containsAll(metaTypes))) - .toList(); - } - - private static List filterMDocByMetadata(Map metaData, List credentialsList) { - MDocMetaData mDocMetaData = MDocMetaData.fromMeta(metaData); - return credentialsList.stream() - .filter(mDocCredential -> mDocCredential.getDocType().equals(mDocMetaData.getDocType())) - .toList(); - } public static boolean containsClaims(CredentialQuery credentialQuery) { return credentialQuery.getClaims() != null && !credentialQuery.getClaims().isEmpty(); diff --git a/src/main/java/io/github/wistefan/dcql/DcSdJwtCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/DcSdJwtCredentialEvaluator.java new file mode 100644 index 0000000..278f4ac --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/DcSdJwtCredentialEvaluator.java @@ -0,0 +1,11 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.CredentialFormat; + +public class DcSdJwtCredentialEvaluator extends SdJwtCredentialEvaluator { + @Override + public CredentialFormat supportedFormat() { + return CredentialFormat.DC_SD_JWT; + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/JwtCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/JwtCredentialEvaluator.java new file mode 100644 index 0000000..3491d34 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/JwtCredentialEvaluator.java @@ -0,0 +1,67 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.*; +import io.github.wistefan.dcql.model.credential.JwtCredential; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.github.wistefan.dcql.DCQLEvaluator.*; + +/** + * Evaluator implementation for JWT Credentials + */ +public class JwtCredentialEvaluator implements CredentialEvaluator { + @Override + public CredentialFormat supportedFormat() { + return CredentialFormat.JWT_VC_JSON; + } + + @Override + public List translate(List credentials) { + return CredentialMapper.toJWTCredentials(credentials); + + } + + @Override + public List evaluate(CredentialQuery credentialQuery, List jwtCredentials) { + if (containsMeta(credentialQuery)) { + jwtCredentials = filterJwtByMetadata(credentialQuery.getMeta(), jwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + jwtCredentials = jwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + jwtCredentials = evaluateJwtCredentialsClaimQuery(cq, jwtCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, jwtCredentials, JwtCredentialEvaluator::evaluateJwtCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.JWT_VC_JSON, jwtCredentials); + } + + private static List filterJwtByMetadata(Map metaData, List credentialsList) { + W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(jwtCredential -> + w3CMetaData.getTypeValues() + .stream() + .anyMatch(metaTypes -> new HashSet<>(jwtCredential.getType()).containsAll(metaTypes))) + .toList(); + } + + private static List evaluateJwtCredentialsClaimQuery(ClaimsQuery cq, List jwtCredentials) { + return jwtCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } +} diff --git a/src/main/java/io/github/wistefan/dcql/LdpCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/LdpCredentialEvaluator.java new file mode 100644 index 0000000..dccb7a5 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/LdpCredentialEvaluator.java @@ -0,0 +1,65 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.*; +import io.github.wistefan.dcql.model.credential.LdpCredential; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.github.wistefan.dcql.DCQLEvaluator.*; + +/** + * Evaluator for LDP Credentials + */ +public class LdpCredentialEvaluator implements CredentialEvaluator { + + @Override + public CredentialFormat supportedFormat() { + return CredentialFormat.LDP_VC; + } + + @Override + public List translate(List credentials) { + return CredentialMapper.toLdpCredentials(credentials); + } + + @Override + public List evaluate(CredentialQuery credentialQuery, List ldpCredentials) { + + if (containsMeta(credentialQuery)) { + ldpCredentials = filterLdpByMetadata(credentialQuery.getMeta(), ldpCredentials); + } + + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + ldpCredentials = evaluateLdpCredentialsClaimQuery(cq, ldpCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, ldpCredentials, LdpCredentialEvaluator::evaluateLdpCredentialsClaimQuery); + } + + return CredentialMapper.toCredentials(CredentialFormat.LDP_VC, ldpCredentials); + } + + + private static List filterLdpByMetadata(Map metaData, List credentialsList) { + W3CMetaData w3CMetaData = W3CMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(ldpCredential -> + w3CMetaData.getTypeValues() + .stream() + .anyMatch(metaTypes -> new HashSet<>(ldpCredential.getType()).containsAll(metaTypes))) + .toList(); + } + + private static List evaluateLdpCredentialsClaimQuery(ClaimsQuery cq, List ldpCredentials) { + return ldpCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForLdpCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/MDocCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/MDocCredentialEvaluator.java new file mode 100644 index 0000000..5042fb5 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/MDocCredentialEvaluator.java @@ -0,0 +1,91 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.*; +import io.github.wistefan.dcql.model.credential.MDocCredential; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.github.wistefan.dcql.DCQLEvaluator.*; + +/** + * Evaluator for MDoc Credentials + */ +public class MDocCredentialEvaluator implements CredentialEvaluator { + + // key to the namespaces in an MDoc credential + private static final String MDOC_NAMESPACE_KEY = "namespaces"; + + @Override + public CredentialFormat supportedFormat() { + return CredentialFormat.MSO_MDOC; + } + + @Override + public List translate(List credentials) { + return CredentialMapper.toMDocCredentials(credentials); + } + + @Override + public List evaluate(CredentialQuery credentialQuery, List mDocCredentials) { + if (containsMeta(credentialQuery)) { + mDocCredentials = filterMDocByMetadata(credentialQuery.getMeta(), mDocCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + mDocCredentials = mDocCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForMDocCredential(taq, credential)) + .toList(); + } + } + translateMDocQueries(credentialQuery); + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + for (ClaimsQuery cq : credentialQuery.getClaims()) { + mDocCredentials = evaluateMDocCredentialsClaimQuery(cq, mDocCredentials); + } + } else if (containsClaims(credentialQuery)) { + return evaluateForClaimSet(credentialQuery, mDocCredentials, MDocCredentialEvaluator::evaluateMDocCredentialsClaimQuery); + } + return CredentialMapper.toCredentials(CredentialFormat.MSO_MDOC, mDocCredentials); + } + + private static List filterMDocByMetadata(Map metaData, List credentialsList) { + MDocMetaData mDocMetaData = MDocMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(mDocCredential -> mDocCredential.getDocType().equals(mDocMetaData.getDocType())) + .toList(); + } + + private static CredentialQuery translateMDocQueries(CredentialQuery credentialQuery) { + if (credentialQuery.getClaims() == null) { + return credentialQuery; + } + credentialQuery.getClaims() + .forEach(cq -> { + if (isMDocClaimsQuery(cq) && cq.getNamespace() != null) { + cq.setPath(List.of(MDOC_NAMESPACE_KEY, cq.getNamespace(), cq.getClaimName())); + } else { + cq.getPath().addFirst(MDOC_NAMESPACE_KEY); + } + }); + return credentialQuery; + } + + private static List evaluateMDocCredentialsClaimQuery(ClaimsQuery cq, List mDocCredentials) { + return mDocCredentials.stream() + .map(credential -> ClaimsEvaluator.evaluateClaimsForMDocCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + } + + private static boolean isMDocClaimsQuery(ClaimsQuery claimsQuery) { + if ((claimsQuery.getNamespace() != null && claimsQuery.getClaimName() == null) || (claimsQuery.getNamespace() == null && claimsQuery.getClaimName() != null)) { + throw new IllegalArgumentException("When a namespace or claim_name is set, the other parameter is mandatory."); + } + return claimsQuery.getIntent_to_retain() != null || claimsQuery.getNamespace() != null; + } + + +} diff --git a/src/main/java/io/github/wistefan/dcql/SdJwtCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/SdJwtCredentialEvaluator.java new file mode 100644 index 0000000..c5640ae --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/SdJwtCredentialEvaluator.java @@ -0,0 +1,99 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.*; +import io.github.wistefan.dcql.model.credential.Disclosure; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; + +import java.util.*; +import java.util.stream.Collectors; + +import static io.github.wistefan.dcql.DCQLEvaluator.*; + +/** + * Evaluator implementation for SD-JWT Credentials + */ +public abstract class SdJwtCredentialEvaluator implements CredentialEvaluator { + + @Override + public List translate(List credentials) { + return CredentialMapper.toSdJWTCredentials(credentials); + } + + @Override + public List evaluate(CredentialQuery credentialQuery, List sdJwtCredentials) { + if (containsMeta(credentialQuery)) { + sdJwtCredentials = filterSdJwtByMetadata(credentialQuery.getMeta(), sdJwtCredentials); + } + if (containsTrustAuthorities(credentialQuery)) { + for (TrustedAuthorityQuery taq : credentialQuery.getTrustedAuthorities()) { + sdJwtCredentials = sdJwtCredentials.stream() + .filter(credential -> TrustedAuthoritiesEvaluator.evaluateQueryForSDJwtCredential(taq, credential)) + .toList(); + } + } + if (containsClaims(credentialQuery) && !containsClaimSets(credentialQuery)) { + sdJwtCredentials = evaluateSdJwtCredentialsQuery(credentialQuery, sdJwtCredentials); + } else if (containsClaims(credentialQuery)) { + return evaluateSdJwtForClaimSet(credentialQuery, sdJwtCredentials); + } else { + sdJwtCredentials = sdJwtCredentials.stream() + // keep the original credential untouched + .map(sdJwtCredential -> new SdJwtCredential(sdJwtCredential.getRaw(), sdJwtCredential.getJwtCredential(), List.of())) + .toList(); + } + return CredentialMapper.toCredentials(credentialQuery.getFormat(), sdJwtCredentials); + } + + private static List filterSdJwtByMetadata(Map metaData, List credentialsList) { + JwtMetaData jwtMetaData = JwtMetaData.fromMeta(metaData); + return credentialsList.stream() + .filter(sdJwtCredential -> jwtMetaData.getVctValues().contains(sdJwtCredential.getVct())) + .toList(); + } + + private static List evaluateSdJwtCredentialsQuery(CredentialQuery credentialQuery, List sdJwtCredentials) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set selectedDisclosures = credentialQuery.getClaims() + .stream() + .map(cq -> ClaimsEvaluator.evaluateClaimsForSdJwtCredential(cq, credential)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(SdJwtCredential::getDisclosures) + .flatMap(List::stream) + .collect(Collectors.toSet()); + disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(selectedDisclosures))); + } + return disclosedCredentials; + } + + + private static List evaluateSdJwtForClaimSet(CredentialQuery credentialQuery, List sdJwtCredentials) { + Map claimsQueryMap = new HashMap<>(); + credentialQuery.getClaims() + .forEach(cq -> claimsQueryMap.put(cq.getId(), cq)); + + for (List claimSet : credentialQuery.getClaimSets()) { + List disclosedCredentials = new ArrayList<>(); + for (SdJwtCredential credential : sdJwtCredentials) { + Set disclosures = new HashSet<>(); + for (String claimId : claimSet) { + ClaimsQuery claimsQuery = claimsQueryMap.get(claimId); + disclosures.addAll(new HashSet<>( + ClaimsEvaluator.evaluateClaimsForSdJwtCredential(claimsQuery, credential) + .map(SdJwtCredential::getDisclosures) + .orElse(new ArrayList<>()))); + } + if (!disclosures.isEmpty()) { + disclosedCredentials.add(new SdJwtCredential(credential.getRaw(), credential.getJwtCredential(), new ArrayList<>(disclosures))); + } + } + + if (!disclosedCredentials.isEmpty()) { + return CredentialMapper.toCredentials(credentialQuery.getFormat(), disclosedCredentials); + } + } + return List.of(); + } + +} diff --git a/src/main/java/io/github/wistefan/dcql/VcSdJwtCredentialEvaluator.java b/src/main/java/io/github/wistefan/dcql/VcSdJwtCredentialEvaluator.java new file mode 100644 index 0000000..0e956d9 --- /dev/null +++ b/src/main/java/io/github/wistefan/dcql/VcSdJwtCredentialEvaluator.java @@ -0,0 +1,10 @@ +package io.github.wistefan.dcql; + +import io.github.wistefan.dcql.model.CredentialFormat; + +public class VcSdJwtCredentialEvaluator extends SdJwtCredentialEvaluator { + @Override + public CredentialFormat supportedFormat() { + return CredentialFormat.VC_SD_JWT; + } +} diff --git a/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java b/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java index 183df67..4cc7832 100644 --- a/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/ClaimsQuery.java @@ -6,6 +6,9 @@ import java.util.List; +/** + * Pojo containing the structur of a claims-query {@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.3} + */ @Data @NoArgsConstructor public class ClaimsQuery { diff --git a/src/main/java/io/github/wistefan/dcql/model/Credential.java b/src/main/java/io/github/wistefan/dcql/model/Credential.java index 9e84e07..ace5663 100644 --- a/src/main/java/io/github/wistefan/dcql/model/Credential.java +++ b/src/main/java/io/github/wistefan/dcql/model/Credential.java @@ -5,6 +5,9 @@ import lombok.Data; import lombok.NoArgsConstructor; +/** + * General holder of all credentials together with their format + */ @Data @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java index 4578696..5c05f20 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialFormat.java @@ -4,6 +4,9 @@ import java.util.Arrays; +/** + * Format of a concrete Verifiable Credential + */ public enum CredentialFormat { MSO_MDOC("mso_mdoc"), diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java index 8948891..c7b6887 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialQuery.java @@ -8,6 +8,7 @@ /** * A Credential Query is an object representing a request for a presentation of one or more matching Credentials. + * {@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1} */ @Data public class CredentialQuery { diff --git a/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java index f27eb0f..b4ddb29 100644 --- a/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/CredentialSetQuery.java @@ -8,6 +8,7 @@ /** * A Credential Set Query is an object representing a request for one or more Credentials to satisfy a particular use * case with the Verifier. + * {@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.2} */ @Data public class CredentialSetQuery { diff --git a/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java index 2ed765c..c9980be 100644 --- a/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/DcqlQuery.java @@ -7,6 +7,7 @@ /** * A JSON-encoded query that allows the Verifier to request presentations that match the query. + * {@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-digital-credentials-query-l} */ @Data public class DcqlQuery { diff --git a/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java b/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java index f71dd63..5811f8b 100644 --- a/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java +++ b/src/main/java/io/github/wistefan/dcql/model/JwtMetaData.java @@ -6,25 +6,31 @@ import java.util.*; +/** + * Holder of metadata-queries for the JWT formats(sd-jwt and jwt) + */ @Data @AllArgsConstructor @NoArgsConstructor public class JwtMetaData { - private static final String VCT_VALUES_KEY = "vct_values"; + private static final String VCT_VALUES_KEY = "vct_values"; - private Set vctValues; + private Set vctValues; - public static JwtMetaData fromMeta(Map metaData) { - if (metaData.containsKey(VCT_VALUES_KEY) && metaData.get(VCT_VALUES_KEY) instanceof List vctValues) { - List vctStrings = vctValues.stream() - .filter(String.class::isInstance) - .map(String.class::cast) - .toList(); - if (vctValues.size() != vctStrings.size()) { - throw new IllegalArgumentException(String.format("The vct_values %s contain invalid values.", vctValues)); - } - return new JwtMetaData(new HashSet<>(vctStrings)); - } - throw new IllegalArgumentException(String.format("Given metaData %s is not sdJwt-metadata.", metaData)); - } + /** + * Extract the supported metadata information for jwt credentials. + */ + public static JwtMetaData fromMeta(Map metaData) { + if (metaData.containsKey(VCT_VALUES_KEY) && metaData.get(VCT_VALUES_KEY) instanceof List vctValues) { + List vctStrings = vctValues.stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + if (vctValues.size() != vctStrings.size()) { + throw new IllegalArgumentException(String.format("The vct_values %s contain invalid values.", vctValues)); + } + return new JwtMetaData(new HashSet<>(vctStrings)); + } + throw new IllegalArgumentException(String.format("Given metaData %s is not sdJwt-metadata.", metaData)); + } } \ No newline at end of file diff --git a/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java b/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java index 619bd34..438fce3 100644 --- a/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java +++ b/src/main/java/io/github/wistefan/dcql/model/MDocMetaData.java @@ -6,6 +6,9 @@ import java.util.Map; +/** + * Holder of metadata-queries for the MDoc format + */ @Data @AllArgsConstructor @NoArgsConstructor @@ -15,6 +18,9 @@ public class MDocMetaData { private String docType; + /** + * Extract the supported metadata(e.g. doctype_value) information for MDoc credentials. + */ public static MDocMetaData fromMeta(Map metaData) { if (metaData.containsKey(DOCTYPE_KEY) && metaData.get(DOCTYPE_KEY) instanceof String docType) { return new MDocMetaData(docType); diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java index 9737710..ff62736 100644 --- a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityQuery.java @@ -11,6 +11,7 @@ * An object representing information that helps to identify an authority or the trust framework that certifies Issuers. * A Credential is identified as a match to a Trusted Authorities Query if it matches with one of the provided values in * one of the provided types. + * {@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1.1} */ @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java index 57285b6..b38dfdc 100644 --- a/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java +++ b/src/main/java/io/github/wistefan/dcql/model/TrustedAuthorityType.java @@ -4,6 +4,9 @@ import java.util.Arrays; +/** + * Type of trusted authorities to be used in queries{@see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1.1} + */ public enum TrustedAuthorityType { AKI("aki"), diff --git a/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java b/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java index 7170fb7..1b3877c 100644 --- a/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java +++ b/src/main/java/io/github/wistefan/dcql/model/W3CMetaData.java @@ -4,10 +4,12 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.HashSet; import java.util.List; import java.util.Map; +/** + * Holder of metadata-queries for the W3C credential formats(ldp, jwt, sd-jwt) + */ @Data @AllArgsConstructor @NoArgsConstructor @@ -17,6 +19,9 @@ public class W3CMetaData { private List> typeValues; + /** + * Extract the supported metadata(e.g. type_values) information for W3C credentials. + */ public static W3CMetaData fromMeta(Map metaData) { if (metaData.containsKey(TYPE_VALUES_KEY) && metaData.get(TYPE_VALUES_KEY) instanceof List typeValues) { diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java b/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java index 3d2b57c..9dd75aa 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/CredentialBase.java @@ -3,6 +3,9 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +/** + * Base class for all credentials. Provides access to the raw credential, without any deserialization applied. + */ @Getter @RequiredArgsConstructor public abstract class CredentialBase { diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java index 1aeed0e..a67b6ad 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/Disclosure.java @@ -8,6 +8,9 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; +/** + * Disclosure object to provide values for an SD-JWT. + */ @Data @EqualsAndHashCode public class Disclosure { @@ -16,6 +19,7 @@ public class Disclosure { private Object value; // the plain, encoded disclosure as it was provided in the original credential private final String encodedDisclosure; + // the sd_hash of the disclosure, correlating with an _sd entry of the credential private final String sdHash; public Disclosure(String salt, String claim, Object value, String encodedDisclosure, String sdAlgorithm) { @@ -26,6 +30,9 @@ public Disclosure(String salt, String claim, Object value, String encodedDisclos this.sdHash = generateSdHash(sdAlgorithm); } + /** + * Generate the hash of the disclosure, based on the algorithm(configured in the credential) + */ private String generateSdHash(String sdAlgorithm) { byte[] disclosureBytes = encodedDisclosure.getBytes(StandardCharsets.UTF_8); MessageDigest digest = getMessageDigest(sdAlgorithm); diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java index 055601d..e1d4ea1 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/JwtCredential.java @@ -1,12 +1,14 @@ package io.github.wistefan.dcql.model.credential; -import lombok.*; -import lombok.extern.slf4j.Slf4j; +import lombok.Getter; import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; +/** + * Holder of JwtCredentials, providing access to its deserialized contents. + */ @Getter public class JwtCredential extends CredentialBase { @@ -15,9 +17,9 @@ public class JwtCredential extends CredentialBase { private static final String TYPE_KEY = "type"; private static final String X5C_KEY = "x5c"; - private Map headers; - private Map payload; - private String signature; + private final Map headers; + private final Map payload; + private final String signature; public JwtCredential(String raw, Map headers, Map payload, String signature) { super(raw); @@ -26,6 +28,9 @@ public JwtCredential(String raw, Map headers, Map getX5Chain() { if (headers.containsKey(X5C_KEY) && headers.get(X5C_KEY) instanceof List x5Chain) { List x509Certificates = x5Chain.stream() @@ -41,6 +46,9 @@ public List getX5Chain() { return List.of(); } + /** + * Returns the concrete "vc" entry from the payload. + */ public Map getPayload() { if (payload.containsKey(VC_PAYLOAD_KEY)) { return (Map) payload.get(VC_PAYLOAD_KEY); @@ -48,6 +56,9 @@ public Map getPayload() { return payload; } + /** + * Returns contents of the "type" field from the credential + */ public List getType() { if (getPayload().containsKey(TYPE_KEY)) { if (getPayload().get(TYPE_KEY) instanceof String typeString) { @@ -65,6 +76,9 @@ public List getType() { throw new IllegalArgumentException("The type field contains invalid entries."); } + /** + * Returns contents of the "vct" field of the credential. + */ public String getVct() { if (getPayload().containsKey(VCT_KEY) && getPayload().get(VCT_KEY) instanceof String vctValue) { return vctValue; diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java index 2a9dd12..35f66ea 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/LdpCredential.java @@ -5,6 +5,9 @@ import java.util.List; import java.util.Map; +/** + * Holder of LdpCredentials, providing access to its deserialized contents. + */ @Getter public class LdpCredential extends CredentialBase { @@ -17,6 +20,9 @@ public LdpCredential(String raw, Map theCredential) { this.theCredential = theCredential; } + /** + * Returns contents of the "type" field from the credential + */ public List getType() { if (theCredential.containsKey(TYPE_KEY)) { if (theCredential.get(TYPE_KEY) instanceof String typeString) { diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java index 9b0e3bb..a7a927e 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/MDocCredential.java @@ -4,6 +4,9 @@ import java.util.Map; +/** + * Holder of MDocCredentials, providing access to its deserialized contents. + */ @Getter public class MDocCredential extends CredentialBase { @@ -18,6 +21,9 @@ public MDocCredential(String raw, MDocHeaders headers, Map paylo this.payload = payload; } + /** + * Returns contents of the "docType" field from the credential + */ public String getDocType() { if (payload.containsKey(DOC_TYPE_KEY) && payload.get(DOC_TYPE_KEY) instanceof String docType) { return docType; diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java b/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java index dba9a3d..9aa2565 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/MDocHeaders.java @@ -7,6 +7,9 @@ import java.security.cert.X509Certificate; import java.util.List; +/** + * Holds MDoc-Specific headers and provides access to them + */ @Data @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java index 3f967c3..af9cd89 100644 --- a/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java +++ b/src/main/java/io/github/wistefan/dcql/model/credential/SdJwtCredential.java @@ -2,16 +2,24 @@ import lombok.Getter; -import java.util.Base64; import java.util.List; import java.util.StringJoiner; +/** + * Holder of SdJwtCredential, providing access to its deserialized contents. + */ @Getter public class SdJwtCredential extends CredentialBase { - private static final String SD_JWT_SEPERATOR = "~"; + private static final String SD_JWT_SEPARATOR = "~"; + /** + * The "standard"-jwt contents of the credential + */ private JwtCredential jwtCredential; + /** + * The disclosures to provide access to the contents + */ private List disclosures; public SdJwtCredential(String raw, JwtCredential jwtCredential, List disclosures) { @@ -29,15 +37,15 @@ public String getRaw() { if (raw == null) { return null; } - String[] splittedRaw = super.getRaw().split(SD_JWT_SEPERATOR); - StringJoiner sdJoiner = new StringJoiner(SD_JWT_SEPERATOR); + String[] splittedRaw = super.getRaw().split(SD_JWT_SEPARATOR); + StringJoiner sdJoiner = new StringJoiner(SD_JWT_SEPARATOR); // first element is the plain jwt. sdJoiner.add(splittedRaw[0]); disclosures.stream() .map(Disclosure::getEncodedDisclosure) .forEach(sdJoiner::add); // the sd needs to end with an ~ - return sdJoiner + SD_JWT_SEPERATOR; + return sdJoiner + SD_JWT_SEPARATOR; } public String getVct() { diff --git a/src/test/java/io/github/wistefan/dcql/example/ParseCredentialTest.java b/src/test/java/io/github/wistefan/dcql/example/ParseCredentialTest.java new file mode 100644 index 0000000..be9ca47 --- /dev/null +++ b/src/test/java/io/github/wistefan/dcql/example/ParseCredentialTest.java @@ -0,0 +1,94 @@ +package io.github.wistefan.dcql.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.SignedJWT; +import io.github.wistefan.dcql.model.Credential; +import io.github.wistefan.dcql.model.CredentialFormat; +import io.github.wistefan.dcql.model.credential.Disclosure; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +public class ParseCredentialTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Test + public void readJwtCredential() throws Exception { + String jwtPath = "example/userCredential.jwt"; + String rawContent = loadFromFile(jwtPath); + SignedJWT signedJWT = SignedJWT.parse(rawContent); + assertDoesNotThrow(() -> { + JwtCredential jwtCredential = new JwtCredential(rawContent, signedJWT.getHeader().toJSONObject(), signedJWT.getJWTClaimsSet().toJSONObject(), signedJWT.getSignature().decodeToString()); + new Credential(CredentialFormat.JWT_VC_JSON, jwtCredential); + }); + } + + @Test + public void readSdJwtCredential() throws Exception { + String sdJwtPath = "example/legalPerson.sd_jwt"; + String rawContent = loadFromFile(sdJwtPath); + + assertDoesNotThrow(() -> { + // split by disclosure separator + String[] sdParts = rawContent.split("~"); + // parse the plain JWT + SignedJWT signedJWT = SignedJWT.parse(sdParts[0]); + Object algorithmClaim = signedJWT.getJWTClaimsSet().getClaim("_sd_alg"); + + // decode the disclosures + List disclosures = Arrays.asList(sdParts) + // everything after the first element + .subList(1, sdParts.length) + .stream() + .map(disclosure -> { + try { + return toDisclosure(disclosure, algorithmClaim); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .toList(); + SdJwtCredential sdJwtCredential = new SdJwtCredential(rawContent, + new JwtCredential(rawContent, signedJWT.getHeader().toJSONObject(), signedJWT.getJWTClaimsSet().toJSONObject(), signedJWT.getSignature().decodeToString()), + disclosures); + }); + } + + private static String loadFromFile(String path) throws IOException { + try (InputStream is = ParseCredentialTest.class.getClassLoader().getResourceAsStream(path)) { + if (is == null) { + throw new IllegalArgumentException("Resource not found: " + path); + } + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + + // decode the encoded disclosure + private Disclosure toDisclosure(String encoded, Object sdAlgorithm) throws IOException { + byte[] sdBytes = Base64.getUrlDecoder().decode(encoded); + List sdContents = OBJECT_MAPPER.readValue(sdBytes, List.class); + String salt = null; + String claim = null; + if (sdContents.get(0) instanceof String saltElement) { + salt = saltElement; + } + if (sdContents.get(1) instanceof String claimElement) { + claim = claimElement; + } + if (sdAlgorithm instanceof String sdAlgorithmString) { + return new Disclosure(salt, claim, sdContents.get(2), encoded, sdAlgorithmString); + } + throw new IllegalArgumentException("Was not able to create disclosure."); + } +} diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java index 38f50bf..81bd4fc 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlClaimSetQueryTest.java @@ -1,17 +1,18 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.wistefan.dcql.DCQLEvaluator; -import io.github.wistefan.dcql.QueryResult; +import io.github.wistefan.dcql.*; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; import io.github.wistefan.dcql.model.DcqlQuery; -import io.github.wistefan.dcql.model.credential.*; +import io.github.wistefan.dcql.model.credential.JwtCredential; +import io.github.wistefan.dcql.model.credential.MDocCredential; +import io.github.wistefan.dcql.model.credential.MDocHeaders; +import io.github.wistefan.dcql.model.credential.SdJwtCredential; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.Base64; import java.util.List; import java.util.Map; @@ -19,6 +20,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { + private static final String MDOC_MVRC_QUERY = """ { "credentials": [ @@ -65,7 +67,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { } """; - private static final Credential MDOC_MVRC_FULL = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + private static final Credential MDOC_MVRC_FULL = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( "docType", "org.iso.7367.1.mVRC", "namespaces", Map.of( "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer"), @@ -75,7 +77,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { "cryptographic_holder_binding", true ))); - private static final Credential MDOC_MVRC_HOLDER = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + private static final Credential MDOC_MVRC_HOLDER = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( "docType", "org.iso.7367.1.mVRC", "namespaces", Map.of( "org.iso.7367.1", Map.of("vehicle_holder", "Martin Auer") @@ -84,7 +86,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { "cryptographic_holder_binding", true ))); - private static final Credential MDOC_MVRC_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + private static final Credential MDOC_MVRC_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( "docType", "org.iso.7367.1.mVRC", "namespaces", Map.of( "org.iso.18013.5.1", Map.of("first_name", "Martin", "last_name", "Auer") @@ -93,7 +95,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { "cryptographic_holder_binding", true ))); - private static final Credential MDOC_MVRC_LAST_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential( null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( + private static final Credential MDOC_MVRC_LAST_NAME = new Credential(CredentialFormat.MSO_MDOC, new MDocCredential(null, new MDocHeaders(null, List.of(generateTestCertificate(TEST_KEY))), Map.of( "docType", "org.iso.7367.1.mVRC", "namespaces", Map.of( "org.iso.18013.5.1", Map.of("last_name", "Auer") @@ -148,8 +150,8 @@ public class DcqlClaimSetQueryTest extends DcqlTest { } """; private static final Credential SD_JWT_VC_FULL = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( null, - new JwtCredential( null, null, + new SdJwtCredential(null, + new JwtCredential(null, null, Map.of( "vct", "https://credentials.example.com/identity_credential", "name", Map.of("_sd", List.of(getDisclosure("salt-b", "first_name", "Arthur").getSdHash(), getDisclosure("salt-c", "last_name", "Dent").getSdHash())), @@ -161,8 +163,8 @@ public class DcqlClaimSetQueryTest extends DcqlTest { )); private static final Credential SD_JWT_VC_ADDRESS = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( null, - new JwtCredential( null, null, + new SdJwtCredential(null, + new JwtCredential(null, null, Map.of( "vct", "https://credentials.example.com/address_credential", "_sd", List.of( @@ -174,8 +176,8 @@ public class DcqlClaimSetQueryTest extends DcqlTest { )); private static final Credential SD_JWT_VC_NAME = new Credential(CredentialFormat.VC_SD_JWT, - new SdJwtCredential( null, - new JwtCredential( null, null, + new SdJwtCredential(null, + new JwtCredential(null, null, Map.of( "vct", "https://credentials.example.com/name_credential", "_sd", List.of( @@ -191,7 +193,7 @@ public class DcqlClaimSetQueryTest extends DcqlTest { @DisplayName("sd-jwt query get alternative") void sdJwtQueryGetAlternative() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -208,7 +210,7 @@ void sdJwtQueryGetAlternative() throws JsonProcessingException { @DisplayName("sd-jwt query get for name") void sdJwtQueryForName() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ALTERNATIVES, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_ADDRESS, SD_JWT_VC_FULL)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -224,7 +226,7 @@ void sdJwtQueryForName() throws JsonProcessingException { @DisplayName("sd-jwt query get for street_address within full") void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_NAME, SD_JWT_VC_FULL)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -240,7 +242,7 @@ void sdJwtQueryForStreetAddressInFull() throws JsonProcessingException { @DisplayName("sd-jwt query get for street_address") void sdJwtQueryForStreetAddress() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_QUERY_ADDRESS, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC_ADDRESS, SD_JWT_VC_NAME, SD_JWT_VC_FULL)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -257,7 +259,7 @@ void sdJwtQueryForStreetAddress() throws JsonProcessingException { @DisplayName("mdoc mvrc query get full doc") void mdocMvrcQueryFullDocSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_FULL, MDOC_MVRC_HOLDER)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); Credential credential = queryResult.credentials().get("credentials").get(0); @@ -268,7 +270,7 @@ void mdocMvrcQueryFullDocSet() throws JsonProcessingException { @DisplayName("mdoc mvrc query get second set") void mdocMvrcQuerySecondSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -280,7 +282,7 @@ void mdocMvrcQuerySecondSet() throws JsonProcessingException { @DisplayName("mdoc mvrc query gets the fullfilling credentials.") void mdocMvrcQueryOnlyOne() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); assertTrue(queryResult.success()); assertEquals(2, queryResult.credentials().get("credentials").size()); @@ -293,7 +295,7 @@ void mdocMvrcQueryOnlyOne() throws JsonProcessingException { @DisplayName("mdoc mvrc query fails when multiple credentials match, but multiple is not allowed.") void mdocMvrcQueryFailedMultiple() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY_SINGLE, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_LAST_NAME, MDOC_MVRC_HOLDER, MDOC_MVRC_NAME, MDOC_MVRC_FULL)); assertFalse(queryResult.success()); } diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java index 4ba97ef..b5c0320 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryComplexTest.java @@ -2,7 +2,6 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; -import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; @@ -129,7 +128,7 @@ class DcqlQueryComplexTest extends DcqlTest { void failsWithNoCredentials() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of()); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of()); assertFalse(queryResult.success()); } @@ -139,7 +138,7 @@ void failsWithNoCredentials() throws JsonProcessingException { void failsWithCredentialsThatDoNotSatisfyARequiredClaimSet() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ADDRESS)); assertFalse(queryResult.success()); } @@ -151,7 +150,7 @@ void succeedsWithRequestedSets() throws JsonProcessingException { List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of( MDOC_MDL_ID, MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ID, @@ -182,7 +181,7 @@ void returnAlternative() throws JsonProcessingException { List expectedPoaCredentials = List.of(MDOC_MDL_ADDRESS); var query = OBJECT_MAPPER.readValue(COMPLEX_MDOC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of( + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of( MDOC_MDL_ADDRESS, MDOC_PHOTO_CARD_ID, MDOC_PHOTO_CARD_ADDRESS, diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java index f261bcc..7511109 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTest.java @@ -2,7 +2,6 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; -import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; @@ -158,7 +157,7 @@ class DcqlQueryTest extends DcqlTest { @DisplayName("mdoc mvrc query fails with invalid mdoc") void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC)); assertFalse(queryResult.success()); } @@ -167,7 +166,7 @@ void mdocMvrcQueryFailsWithInvalidMdoc() throws JsonProcessingException { @DisplayName("mdoc mvrc example with multiple credentials succeeds") void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, MDOC_MVRC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -178,7 +177,7 @@ void mdocMvrcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingExcep @DisplayName("w3cLdpVc example succeeds") void w3cLdpVcExampleSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_W3C_LDP_VC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -189,7 +188,7 @@ void w3cLdpVcExampleSucceeds() throws JsonProcessingException { @DisplayName("w3cLdpVc query fails with invalid type values") void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(W3C_LDP_VC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); assertFalse(queryResult.success()); } @@ -198,7 +197,7 @@ void w3cLdpVcQueryFailsWithInvalidTypeValues() throws JsonProcessingException { @DisplayName("mdocMvrc example using namespaces succeeds") void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_NAMESPACE_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -209,7 +208,7 @@ void mdocMvrcExampleUsingNamespacesSucceeds() throws JsonProcessingException { void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_MDOC, EXAMPLE_SD_JWT_VC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -227,7 +226,7 @@ void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingExcept void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_SD_JWT_VC)); assertTrue(queryResult.success()); assertEquals(2, queryResult.credentials().get("credentials").size()); } @@ -237,7 +236,7 @@ void sdJwtVcWithMultipleSetToTrueSucceeds() throws JsonProcessingException { void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_MULTIPLE_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -255,7 +254,7 @@ void sdJwtVcWithMultipleButOneMatch() throws JsonProcessingException { void sdJwtVcWithNoClaims() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_NO_CLAIMS_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(EXAMPLE_SD_JWT_VC, EXAMPLE_MDOC)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java index 5cc5232..c61db8a 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryTrustedAuthoritiesTest.java @@ -2,7 +2,6 @@ package io.github.wistefan.dcql.query; import com.fasterxml.jackson.core.JsonProcessingException; -import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; @@ -12,7 +11,6 @@ import io.github.wistefan.dcql.model.credential.MDocHeaders; import io.github.wistefan.dcql.model.credential.SdJwtCredential; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.Base64; @@ -102,7 +100,7 @@ class DcqlQueryTrustedAuthoritiesTest extends DcqlTest { @DisplayName("mdocMvrc example with trusted_authorities succeeds") void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); + QueryResult credentialsResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC)); assertTrue(credentialsResult.success()); assertEquals(1, credentialsResult.credentials().get("credentials").size()); @@ -113,7 +111,7 @@ void mdocMvrcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingExcept void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); + QueryResult credentialsResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_ALT_AKI)); assertFalse(credentialsResult.success()); } @@ -123,7 +121,7 @@ void mdocMvrcExampleWhereAuthorityDoesNotMatch() throws JsonProcessingException void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); + QueryResult credentialsResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_MVRC_NO_X5C)); assertFalse(credentialsResult.success()); } @@ -133,7 +131,7 @@ void mdocMvrcExampleWithNoAuthority() throws JsonProcessingException { void sdJwtVcExampleWithTrustedAuthoritiesSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult credentialsResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); + QueryResult credentialsResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(SD_JWT_VC)); assertTrue(credentialsResult.success()); assertEquals(1, credentialsResult.credentials().get("credentials").size()); diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java index 0fd829b..c45fa1c 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlQueryWithJsonTransformTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; -import io.github.wistefan.dcql.DCQLEvaluator; import io.github.wistefan.dcql.QueryResult; import io.github.wistefan.dcql.model.Credential; import io.github.wistefan.dcql.model.CredentialFormat; @@ -12,7 +11,6 @@ import io.github.wistefan.dcql.model.credential.MDocCredential; import io.github.wistefan.dcql.model.credential.SdJwtCredential; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.List; @@ -111,7 +109,7 @@ public int hashCode() { @DisplayName("mdocMvrc example succeeds") void mdocMvrcExampleSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(MDOC_MVRC_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); @@ -121,7 +119,7 @@ void mdocMvrcExampleSucceeds() throws JsonProcessingException { @DisplayName("sdJwtVc example with multiple credentials succeeds") void sdJwtVcExampleWithMultipleCredentialsSucceeds() throws JsonProcessingException { var query = OBJECT_MAPPER.readValue(SD_JWT_VC_EXAMPLE_QUERY, DcqlQuery.class); - QueryResult queryResult = DCQLEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); + QueryResult queryResult = dcqlEvaluator.evaluateDCQLQuery(query, List.of(MDOC_WITH_JT, SD_JWT_VC_WITH_JT)); assertTrue(queryResult.success()); assertEquals(1, queryResult.credentials().get("credentials").size()); diff --git a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java index e125699..d09a70c 100644 --- a/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java +++ b/src/test/java/io/github/wistefan/dcql/query/DcqlTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.module.SimpleModule; +import io.github.wistefan.dcql.*; import io.github.wistefan.dcql.helper.CredentialFormatDeserializer; import io.github.wistefan.dcql.helper.TrustedAuthorityTypeDeserializer; import io.github.wistefan.dcql.model.CredentialFormat; @@ -19,6 +20,7 @@ import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.junit.jupiter.api.BeforeEach; import java.math.BigInteger; import java.security.KeyPair; @@ -45,6 +47,17 @@ public abstract class DcqlTest { public static final KeyPair TEST_KEY = generateTestKeyPair(); + protected DCQLEvaluator dcqlEvaluator; + + @BeforeEach + public void setUp() { + dcqlEvaluator = new DCQLEvaluator(List.of( + new JwtCredentialEvaluator(), + new DcSdJwtCredentialEvaluator(), + new VcSdJwtCredentialEvaluator(), + new MDocCredentialEvaluator(), + new LdpCredentialEvaluator())); + } public static KeyPair generateTestKeyPair() { try { diff --git a/src/test/resources/example/legalPerson.sd_jwt b/src/test/resources/example/legalPerson.sd_jwt new file mode 100644 index 0000000..e2b2bb7 --- /dev/null +++ b/src/test/resources/example/legalPerson.sd_jwt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAiZGMrc2Qtand0Iiwia2lkIiA6ICJkaWQ6a2V5OnpEbmFlYm9MREZXdTRvYlFGQmZUMWl0YUJZUzhmZDJkNVU1OWo1bzRndHZUY0JOcVEifQ.eyJfc2QiOlsiMTlORWo4M0swYWp1UEpTTjZodWtKbWVXbFY3aXZKdVBSS19VaVNUU2QxMCIsIkJIVkZVZ1ROa2gtbU9GdU8yLTB1MkZwSmZFMWs4dWNIWUkzRXcyMHpma2ciLCJDNTRPSWFJRV95OThQQzduZlR2S1lURWNqWE1tV2R5eWlUZXNPMGw3c2RnIiwiRVkzSVpyaWJDcGR1UzBYN0Y4eXJtNkZIaFVUelNucUFjaDhrSHRwcnFBQSIsIkxwOFhzalRqUUxGRXJnWVJtaFVWdnd0ajlwbno0NXVzUnUtcXY4YVNYWFEiLCJQMHZaNE40SDFTUTFmUUU5TndxVkxvME9LanhlY1UyQ0FQZkdRakxsVjh3IiwiU0E0NllaOFJDbVJIV3d6SE94R3FqdzJHWVg0R1VMbUllQTd1OWZXblctUSIsIlhrUi1JZUx2RmpMS3ZjU1BhQThZTGRoekVIbjl0UGpRWnpFOEJWMFdpLVkiLCJhalFEaWk5bGV5RlNFY3RwVDN6X084ZHJXV1dSSUdKUmhIeEwzTkZyZC13IiwiYXcxYzBXYUhzcElNamJBXzVsWWx4MW5NYjhIQXpGWE4yYW9KUTE4bVdoOCIsImJGNE1PSENxUVVTVGpSNy1XVXpYNlR4eFdCblpRVEQ1N1BHMDhFa1NNc28iLCJiVTdGd1U1YVVKZEZGUTFoV3B6dVlWN0tpdFJYYi1kU01CNDR3N19wY1NrIiwiZHVFWVk1dTlOQ1RDM2l1WkROdmIxdkcxaE9sUDgyZklRNm9yS0dsUDc1dyIsImUwai0xUVBQWWlpVzdXYjQybmpZT243a1JLYWtNblh0YUpxQ2gwajZmXzQiLCJxazhFck8xUmgtd0ljWEZrYlRwQkJ1WHNmZUw0U3JtY3ZLM3VaM20xd1dFIiwidUExdExzaU9UZHN6VWZFLVNOLU52R3dQOWthb1IzOC1CNUZoTzM5TjYwOCIsInVxVG1XVkFnR29ncXJlU2k3dWhGR19SMUdGZ0xkTkNmOFNzdTBlQmM5Y2MiLCJ2RVVoMGVVMFFDUkw3eEZCbUszdXk3QWRmQzlHamVjWWRwaXpGbU4wdC04IiwieFR5cWFaZEQ2R3dHSHp2UGFvZ0JFMFc1REdqWW5ZWEdDUDRrUW1zX1V5dyIsInlXODlyQmtNUjRGdjd0aVpDbGhfc3FXMlZSdU5lT2VlbUE1ODhvdEx6MjgiLCJ6TlJJWFFzb0F4a0RnUTFVVmY0WG5ibEJOTmZ4bThfd0tGdlctVXRXY3hrIl0sIl9zZF9hbGciOiJzaGEtMjU2IiwidmN0IjoiTGVnYWxQZXJzb25DcmVkZW50aWFsIiwiaXNzIjoiZGlkOmtleTp6RG5hZWJvTERGV3U0b2JRRkJmVDFpdGFCWVM4ZmQyZDVVNTlqNW80Z3R2VGNCTnFRIn0.XpOwHENO_E36ssmNtv9iK7l4cKsPbRvM6p-LZtG1WwmcfFiyoO-7N7dmuiADvXldgT0EIX7XN_ATEak5JbYpaQ~WyJwMTRoeWp2WFQzUGx6Q1IyaFozeEhBIiwgImNvdW50cnkiLCAiR2VybWFueSJd~WyJqUmxIT2poMXR2a1VjQU5kbGVlNm93IiwgImxhc3ROYW1lIiwgIlVzZXIiXQ~WyJGYlRSOE1fLTlsZ2NtdndHWUdFYU5nIiwgInN0cmVldE51bWJlciIsICIxMCJd~WyJDVDBRdUxmbzQ1eVJyaHhQWXBxZHZ3IiwgImNpdHkiLCAiRHJlc2RlbiJd~WyJOZ1RJYzkxRXJCWFlMenVwUnhCQVpnIiwgInN1YmplY3QiLCAiZGlkOmtleTp6RG5hZWlWcHhDVDdBUndxTG5kYldpQ2VHRzJZWlh2TGZXRnMxY0dQZ0tVZThHUExlIl0~WyJGQmpGNUxmcWdMWTZoclM4NnVFS1pRIiwgInJvbGVzIiwgW3sibmFtZXMiOiBbIlJFUFJFU0VOVEFUSVZFIiwgIlJFQURFUiIsICJjdXN0b21lciJdLCAidGFyZ2V0IjogImRpZDprZXk6ekRuYWVhSmtFUjhFclRBWnNxcEdLZXBnaWRmdFphNEx5dld2b2dRUGZqMllpU1A2YiJ9XV0~WyJ5QVNOQkw1QjhfT20zTHJOeVNfbUl3IiwgInppcGNvZGUiLCAiMDExNjkiXQ~WyJVTEtjckhVTmFKUEptSkhRYllBZGpnIiwgImZpcnN0TmFtZSIsICJUZXN0Il0~WyJ2UGY2OHViblp2RFRPS29WWkJZX2VnIiwgInN0cmVldCIsICJNYWluIFN0cmVldCJd~WyJ2VjVfTVkyYlBsYkV6SkpCZS1jWm13IiwgInJlZ2lvbiIsICJTYXhvbnkiXQ~WyJjd2F4dUFTUkIwUmN3Qjd5Y3NXYXJ3IiwgImVtYWlsIiwgImVtcGxveWVlQGNvbnN1bWVyLm9yZyJd~ \ No newline at end of file diff --git a/src/test/resources/example/userCredential.jwt b/src/test/resources/example/userCredential.jwt new file mode 100644 index 0000000..6f7e6c2 --- /dev/null +++ b/src/test/resources/example/userCredential.jwt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkaWQ6a2V5OnpEbmFlV0dpVkNRS0pmTjVyUGtRNHk3SFJhUVN3NUFWc2VabzNrUWZzSkhmOUNkWGsifQ.eyJuYmYiOjE3NTkyMjM2NzEsImp0aSI6InVybjp1dWlkOjU5NjEwMjZiLWM4ZDktNGZkZC1iNTlmLWJkYWRhMWQ0NjFhZSIsImlzcyI6ImRpZDprZXk6ekRuYWVXR2lWQ1FLSmZONXJQa1E0eTdIUmFRU3c1QVZzZVpvM2tRZnNKSGY5Q2RYayIsInZjIjp7InR5cGUiOlsiVXNlckNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6RG5hZVdHaVZDUUtKZk41clBrUTR5N0hSYVFTdzVBVnNlWm8za1Fmc0pIZjlDZFhrIiwiaXNzdWFuY2VEYXRlIjoxNzU5MjIzNjcxLjA5ODAwMDAwMCwiY3JlZGVudGlhbFN1YmplY3QiOnsiemlwY29kZSI6IjAxMTY5IiwibGFzdE5hbWUiOiJVc2VyIiwiY291bnRyeSI6Ikdlcm1hbnkiLCJmaXJzdE5hbWUiOiJUZXN0IiwiY2l0eSI6IkRyZXNkZW4iLCJzdHJlZXROdW1iZXIiOiIxMCIsInN0cmVldCI6Ik1haW4gU3RyZWV0Iiwic3ViamVjdCI6ImRpZDprZXk6ekRuYWVpVnB4Q1Q3QVJ3cUxuZGJXaUNlR0cyWVpYdkxmV0ZzMWNHUGdLVWU4R1BMZSIsInJvbGVzIjpbeyJuYW1lcyI6WyJSRVBSRVNFTlRBVElWRSIsIlJFQURFUiIsImN1c3RvbWVyIl0sInRhcmdldCI6ImRpZDprZXk6ekRuYWVYM2RGWktNUnh0THFROEhSQkFRekJzcEJQZHJubVZoRlNNZzNMeE05ZWZ0MSJ9XSwicmVnaW9uIjoiU2F4b255IiwiZW1haWwiOiJlbXBsb3llZUBjb25zdW1lci5vcmcifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjEiXX19.O6j8bOJuK9KXiW-eHjtwJVnCAA7VmUvv9uYCIGwDe2Pxnsm02qyMGri4drdpE_u0_ZMKndUwwerU2QZm5bo0sA \ No newline at end of file