[java-idp-plugin-vci] 02/02: Claims having path depth are now written correctly also to SD JWT VC. Refactoring and helpers.

Codeberg noreply at shibboleth.net
Thu Aug 27 11:49:07 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch fix/SD_PATHS
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/bbcf22bb3b0fab394b8770becfc40ef9e64b5998

commit bbcf22bb3b0fab394b8770becfc40ef9e64b5998
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Aug 27 14:48:45 2026 +0300

    Claims having path depth are now written correctly also to SD JWT VC. Refactoring and helpers.
---
 ...FormJsonLdSelectiveDisclosureJWTCredential.java |  51 ++--
 .../impl/FormSelectiveDisclosureJWTCredential.java |  33 +--
 .../util/SelectiveDisclosureClaimSetUtil.java      | 132 +++++++---
 .../util/SelectiveDisclosureClaimTree.java         | 184 ++++++++++++++
 .../util/SelectiveDisclosureClaimSetUtilTest.java  | 266 +++++++++++++++++++--
 .../util/SelectiveDisclosureClaimTreeTest.java     | 214 +++++++++++++++++
 6 files changed, 772 insertions(+), 108 deletions(-)

diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
index 030c640..ba7bc5e 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
@@ -17,9 +17,7 @@
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 import java.util.function.Function;
 
@@ -27,10 +25,9 @@ import javax.annotation.Nonnull;
 
 import org.geant.shibboleth.plugin.oauth.profile.logic.CredentialIssuerLookupFunction;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
-import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedClaim;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
-import org.geant.shibboleth.plugin.openidvci.util.SelectiveDisclosureClaimSetUtil;
+import org.geant.shibboleth.plugin.openidvci.util.SelectiveDisclosureClaimTree;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -43,6 +40,7 @@ import net.shibboleth.shared.logic.Constraint;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
 /**
  * Action that forms
@@ -123,48 +121,35 @@ public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileA
         return true;
     }
 
-    @SuppressWarnings("unchecked")
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        Map<List<String>, Map<String, Object>> sdMaps = new HashMap<>();
-        for (CredentialOfferRequestedClaim claim : credential.getRequestedCredential()) {
-            List<String> path = new ArrayList<>(claim.getPath());
-            path.remove(path.size() - 1);
-            List<String> key = List.copyOf(path);
-            String leafKey = claim.getPath().get(claim.getPath().size() - 1);
-            sdMaps.computeIfAbsent(key, k -> new HashMap<>()).put(leafKey, claim.getValue());
+        final SelectiveDisclosureClaimTree tree;
+        try {
+            tree = new SelectiveDisclosureClaimTree(credential.getRequestedCredential());
+        } catch (final IllegalArgumentException e) {
+            log.error("{} Unable to disclose the requested claims", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
+            return;
         }
-        String disclosures = null;
-        Map<String, Object> credentialSubject = new HashMap<>();
-        for (Map.Entry<List<String>, Map<String, Object>> entry : sdMaps.entrySet()) {
-            List<String> path = entry.getKey();
-            Map<String, Object> claims = entry.getValue();
-            Map<String, Object> map = credentialSubject;
-            for (String segment : path) {
-                map = (Map<String, Object>) map.computeIfAbsent(segment, k -> new HashMap<String, Object>());
-            }
-            SelectiveDisclosureClaimSetUtil sdActClaims = new SelectiveDisclosureClaimSetUtil(claims);
-            map.put("_sd", sdActClaims.get_sd());
-            disclosures = (disclosures == null) ? sdActClaims.getFormattedDisclosures()
-                    : disclosures + "~" + sdActClaims.getFormattedDisclosures();
-        }
-        List<JWTClaimsSet> credentials = new ArrayList<>();
+        final List<JWTClaimsSet> credentials = new ArrayList<>();
         final String issuer = issuerLookupStrategy.apply(profileRequestContext);
-        ctx.getCredentialShells().forEach(cred -> {
+        for (final ClaimsSet cred : ctx.getCredentialShells()) {
             try {
+                // Claims of this format are inside 'credentialSubject'.
                 credentials.add(new JWTClaimsSet.Builder(cred.toJWTClaimsSet())
                         .claim("@context", ctx.getCredentialConfiguration().getCredentialDefinition().getContext())
                         .claim("id", "urn:uuid:" + UUID.randomUUID())
                         .claim("type", ctx.getCredentialConfiguration().getCredentialDefinition().getType())
-                        .claim("issuer", issuer).claim("credentialSubject", credentialSubject)
-                        .claim("_sd_alg", "sha-256").build());
-            } catch (ParseException e) {
+                        .claim("issuer", issuer).claim("credentialSubject", tree.getClaims())
+                        .claim("_sd_alg", tree.getAlgorithm()).build());
+            } catch (final ParseException e) {
                 log.error("{} Parsing credential failed", getLogPrefix(), e);
                 ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
+                return;
             }
-        });
+        }
         ctx.setJWTCredentials(credentials);
-        ctx.setDisclosures(disclosures);
+        ctx.setDisclosures(tree.getFormattedDisclosures());
     }
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormSelectiveDisclosureJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormSelectiveDisclosureJWTCredential.java
index 4715d6c..490df3b 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormSelectiveDisclosureJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormSelectiveDisclosureJWTCredential.java
@@ -17,16 +17,14 @@
 package org.geant.shibboleth.plugin.openidvci.profile.impl;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import javax.annotation.Nonnull;
 
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
-import org.geant.shibboleth.plugin.openidvci.util.SelectiveDisclosureClaimSetUtil;
+import org.geant.shibboleth.plugin.openidvci.util.SelectiveDisclosureClaimTree;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
@@ -38,6 +36,7 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
 /**
  * Action that forms
@@ -99,22 +98,28 @@ public class FormSelectiveDisclosureJWTCredential extends AbstractProfileAction
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
-        Map<String, Object> claims = new HashMap<String, Object>();
-        // TODO: support for depth of more than 1.
-        credential.getRequestedCredential().forEach(claim -> claims.put(claim.getPath().get(0), claim.getValue()));
-        SelectiveDisclosureClaimSetUtil sdClaims = new SelectiveDisclosureClaimSetUtil(claims);
-        List<JWTClaimsSet> credentials = new ArrayList<>();
-        ctx.getCredentialShells().forEach(cred -> {
+        final SelectiveDisclosureClaimTree tree;
+        try {
+            tree = new SelectiveDisclosureClaimTree(credential.getRequestedCredential());
+        } catch (final IllegalArgumentException e) {
+            log.error("{} Unable to disclose the requested claims", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
+            return;
+        }
+        // Claims of this format are on top level of the credential.
+        final List<JWTClaimsSet> credentials = new ArrayList<>();
+        for (final ClaimsSet cred : ctx.getCredentialShells()) {
             try {
-                credentials.add(new JWTClaimsSet.Builder(cred.toJWTClaimsSet()).claim("_sd", sdClaims.get_sd())
-                        .claim("_sd_alg", sdClaims.get_alg()).build());
-            } catch (ParseException e) {
+                final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(cred.toJWTClaimsSet());
+                tree.getClaims().forEach(builder::claim);
+                credentials.add(builder.claim("_sd_alg", tree.getAlgorithm()).build());
+            } catch (final ParseException e) {
                 log.error("{} Parsing credential failed", getLogPrefix(), e);
                 ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_CREDENTIAL);
                 return;
             }
-        });
+        }
         ctx.setJWTCredentials(credentials);
-        ctx.setDisclosures(sdClaims.getFormattedDisclosures());
+        ctx.setDisclosures(tree.getFormattedDisclosures());
     }
 }
\ No newline at end of file
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtil.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtil.java
index 1dbfdfe..1c0d76a 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtil.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtil.java
@@ -21,80 +21,142 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Base64;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
 import net.shibboleth.shared.security.IdentifierGenerationStrategy;
 import net.shibboleth.shared.security.impl.SecureRandomIdentifierGenerationStrategy;
 
 /**
  * Helper to create SD JWT claims and disclosures.
  *
+ * Claims given to this helper are all on same level. Disclosure is JSON array
+ * of salt, claim name and claim value, and value keeps its type.
  */
 public class SelectiveDisclosureClaimSetUtil {
 
+    /** Claims to disclose. */
+    @Nonnull
     private final Map<String, Object> sdClaims;
 
+    /** Digest algorithm of disclosures. */
+    @Nonnull
     private final String algorithm;
 
+    /** Disclosures, base64url encoded. */
     private List<String> urlEncodedDisclosures;
 
+    /** Digests of disclosures, base64url encoded. */
     private List<String> encodedDisclosures;
 
-    IdentifierGenerationStrategy strategy = new SecureRandomIdentifierGenerationStrategy();
-
-    public SelectiveDisclosureClaimSetUtil(Map<String, Object> claims) {
-        sdClaims = claims;
-        algorithm = "sha-256";
-        process();
+    /** Source of disclosure salts. */
+    @Nonnull
+    private final IdentifierGenerationStrategy strategy = new SecureRandomIdentifierGenerationStrategy();
+
+    /** Mapper serializing disclosures. */
+    @Nonnull
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * Constructor.
+     *
+     * @param claims claims to disclose
+     */
+    public SelectiveDisclosureClaimSetUtil(@Nonnull final Map<String, Object> claims) {
+        this(claims, "sha-256");
     }
 
-    public SelectiveDisclosureClaimSetUtil(Map<String, Object> claims, String algorithm) {
+    /**
+     * Constructor.
+     *
+     * @param claims    claims to disclose
+     * @param algorithm digest algorithm of disclosures
+     */
+    public SelectiveDisclosureClaimSetUtil(@Nonnull final Map<String, Object> claims, @Nonnull final String algorithm) {
         sdClaims = claims;
         this.algorithm = algorithm;
         process();
     }
 
+    /**
+     * Get digests of the disclosures, for '_sd' claim.
+     *
+     * @return digests of the disclosures
+     */
     public List<String> get_sd() {
         return encodedDisclosures;
     }
 
+    /**
+     * Get the disclosures, separated by '~'.
+     *
+     * @return the disclosures
+     */
     public String getFormattedDisclosures() {
         return urlEncodedDisclosures.stream().map(String::valueOf).collect(Collectors.joining("~"));
     }
 
-    private String process() {
-        urlEncodedDisclosures = new ArrayList<String>();
-        encodedDisclosures = new ArrayList<String>();
-        List<String> disclosures = new ArrayList<String>();
-        sdClaims.keySet().forEach(key -> disclosures.add(
-                "[\"" + Base64.getUrlEncoder().withoutPadding().encodeToString(strategy.generateIdentifier().getBytes())
-                        + "\", \"" + key + "\", \"" + sdClaims.get(key).toString() + "\"]"));
-        disclosures.forEach(disclosure -> urlEncodedDisclosures.add(
-                Base64.getUrlEncoder().withoutPadding().encodeToString(disclosure.getBytes(StandardCharsets.UTF_8))));
-        List<byte[]> hashedDisclosures = new ArrayList<byte[]>();
-        urlEncodedDisclosures.forEach(disclosure -> {
-            try {
-                hashedDisclosures
-                        .add(MessageDigest.getInstance("SHA-256").digest(disclosure.getBytes(StandardCharsets.UTF_8)));
-            } catch (NoSuchAlgorithmException e) {
-                // Very unlikely to happen.
-                e.printStackTrace();
-            }
-        });
-        hashedDisclosures.forEach(disclosure -> encodedDisclosures
-                .add(Base64.getUrlEncoder().withoutPadding().encodeToString(disclosure)));
-        String ret = "";
-        for (int i = 0; i < encodedDisclosures.size(); i++) {
-            ret += "~" + encodedDisclosures.get(i);
-        }
-        return ret;
+    /**
+     * Get digest algorithm of disclosures, for '_sd_alg' claim.
+     *
+     * @return digest algorithm of disclosures
+     */
+    public String get_alg() {
+        return algorithm.toLowerCase();
+    }
 
+    /** Form the disclosures and their digests. */
+    private void process() {
+        urlEncodedDisclosures = new ArrayList<>();
+        encodedDisclosures = new ArrayList<>();
+        final MessageDigest digest;
+        try {
+            digest = MessageDigest.getInstance(algorithm);
+        } catch (final NoSuchAlgorithmException e) {
+            throw new IllegalArgumentException("Unsupported disclosure digest algorithm " + algorithm, e);
+        }
+        for (final Map.Entry<String, Object> claim : sdClaims.entrySet()) {
+            final String disclosure = Base64.getUrlEncoder().withoutPadding()
+                    .encodeToString(encode(claim.getKey(), claim.getValue()).getBytes(StandardCharsets.UTF_8));
+            urlEncodedDisclosures.add(disclosure);
+            encodedDisclosures.add(Base64.getUrlEncoder().withoutPadding()
+                    .encodeToString(digest.digest(disclosure.getBytes(StandardCharsets.UTF_8))));
+        }
+        // We mess up the order so it cannot be used to determine anything in '_sd'.
+        Collections.sort(encodedDisclosures);
     }
 
-    public String get_alg() {
-        return algorithm.toLowerCase();
+    /**
+     * Serialize single disclosure as JSON array of salt, claim name and claim
+     * value.
+     *
+     * @param name  name of the claim
+     * @param value value of the claim
+     *
+     * @return the disclosure
+     */
+    @Nonnull
+    private String encode(@Nonnull final String name, final Object value) {
+        final JsonNode encodedValue = objectMapper.valueToTree(value);
+        final ArrayNode disclosure = objectMapper.createArrayNode();
+        disclosure.add(strategy.generateIdentifier());
+        disclosure.add(name);
+        disclosure.add(encodedValue);
+        try {
+            return objectMapper.writeValueAsString(disclosure);
+        } catch (final JsonProcessingException e) {
+            // Serializing a tree of nodes does not fail.
+            throw new IllegalArgumentException("Unable to serialize disclosure of claim " + name, e);
+        }
     }
 
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTree.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTree.java
new file mode 100644
index 0000000..5f70872
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTree.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedClaim;
+
+/**
+ * Helper to create SD JWT claims and disclosures of claims having a path.
+ *
+ * Claims of same parent path are disclosed together and their '_sd' is placed
+ * to that parent. Only leaf claims are disclosed.
+ */
+public class SelectiveDisclosureClaimTree {
+
+    /** Name of the claim carrying the digests. */
+    @Nonnull
+    private static final String SD_CLAIM = "_sd";
+
+    /** Objects on the paths, with '_sd' of the leaf claims. */
+    @Nonnull
+    private final Map<String, Object> claims;
+
+    /** Disclosures of all levels, separated by '~'. */
+    @Nonnull
+    private final String disclosures;
+
+    /** Digest algorithm of disclosures. */
+    @Nonnull
+    private final String algorithm;
+
+    /**
+     * Constructor.
+     *
+     * @param requested claims to disclose
+     *
+     * @throws IllegalArgumentException if the claims cannot form a tree
+     */
+    public SelectiveDisclosureClaimTree(@Nonnull final Collection<CredentialOfferRequestedClaim> requested) {
+        this(requested, "sha-256");
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param requested claims to disclose
+     * @param algorithm digest algorithm of disclosures
+     *
+     * @throws IllegalArgumentException if the claims cannot form a tree
+     */
+    public SelectiveDisclosureClaimTree(@Nonnull final Collection<CredentialOfferRequestedClaim> requested,
+            @Nonnull final String algorithm) {
+
+        this.algorithm = algorithm;
+        validate(requested);
+
+        // Claims of same parent are disclosed together.
+        final Map<List<String>, Map<String, Object>> levels = new LinkedHashMap<>();
+        for (final CredentialOfferRequestedClaim claim : requested) {
+            final List<String> path = claim.getPath();
+            final List<String> parent = List.copyOf(path.subList(0, path.size() - 1));
+            levels.computeIfAbsent(parent, k -> new LinkedHashMap<>()).put(path.get(path.size() - 1), claim.getValue());
+        }
+
+        claims = new LinkedHashMap<>();
+        final List<String> parts = new ArrayList<>();
+        for (final Map.Entry<List<String>, Map<String, Object>> level : levels.entrySet()) {
+            final SelectiveDisclosureClaimSetUtil disclosed = new SelectiveDisclosureClaimSetUtil(level.getValue(),
+                    algorithm);
+            resolve(level.getKey()).put(SD_CLAIM, disclosed.get_sd());
+            parts.add(disclosed.getFormattedDisclosures());
+        }
+        disclosures = String.join("~", parts);
+    }
+
+    /**
+     * Get objects on the paths, with '_sd' of the leaf claims.
+     *
+     * @return the claims
+     */
+    @Nonnull
+    public Map<String, Object> getClaims() {
+        return claims;
+    }
+
+    /**
+     * Get the disclosures of all levels, separated by '~'.
+     *
+     * @return the disclosures
+     */
+    @Nonnull
+    public String getFormattedDisclosures() {
+        return disclosures;
+    }
+
+    /**
+     * Get digest algorithm of disclosures, for '_sd_alg' claim.
+     *
+     * @return digest algorithm of disclosures
+     */
+    @Nonnull
+    public String getAlgorithm() {
+        return algorithm.toLowerCase();
+    }
+
+    /**
+     * Check that the claims can form tree. Same claim twice, claim that is also an
+     * object on the path of another claim, and claim reserving the name of the
+     * digest claim are all rejected. Left unchecked they would silently lose a
+     * claim.
+     *
+     * @param requested claims to disclose
+     *
+     * @throws IllegalArgumentException if the claims cannot form a tree
+     */
+    private void validate(@Nonnull final Collection<CredentialOfferRequestedClaim> requested) {
+
+        final Set<List<String>> objects = new HashSet<>();
+        for (final CredentialOfferRequestedClaim claim : requested) {
+            final List<String> path = claim.getPath();
+            for (int i = 1; i < path.size(); i++) {
+                objects.add(List.copyOf(path.subList(0, i)));
+            }
+        }
+
+        final Set<List<String>> seen = new HashSet<>();
+        for (final CredentialOfferRequestedClaim claim : requested) {
+            final List<String> path = List.copyOf(claim.getPath());
+            if (path.contains(SD_CLAIM)) {
+                throw new IllegalArgumentException(
+                        "Claim " + String.join("/", path) + " uses reserved name " + SD_CLAIM);
+            }
+            if (objects.contains(path)) {
+                throw new IllegalArgumentException(
+                        "Claim " + String.join("/", path) + " is also an object on path of another claim");
+            }
+            if (!seen.add(path)) {
+                throw new IllegalArgumentException("Claim " + String.join("/", path) + " is requested twice");
+            }
+        }
+    }
+
+    /**
+     * Resolve the object of a path, creating objects on the way when they are not
+     * there yet.
+     *
+     * @param path path of the object
+     *
+     * @return the object
+     */
+    @SuppressWarnings("unchecked")
+    @Nonnull
+    private Map<String, Object> resolve(@Nonnull final List<String> path) {
+        Map<String, Object> object = claims;
+        for (final String segment : path) {
+            object = (Map<String, Object>) object.computeIfAbsent(segment, k -> new LinkedHashMap<String, Object>());
+        }
+        return object;
+    }
+
+}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtilTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtilTest.java
index 5481d93..50d5b99 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtilTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimSetUtilTest.java
@@ -1,48 +1,262 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package org.geant.shibboleth.plugin.openidvci.util;
 
-import java.security.NoSuchAlgorithmException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Base64;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+
 import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.nimbusds.jose.jwk.ECKey;
-import com.nimbusds.oauth2.sdk.ParseException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 
 public class SelectiveDisclosureClaimSetUtilTest {
 
-    SelectiveDisclosureClaimSetUtil claimSet;
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    private JsonNode decode(final String disclosure) throws Exception {
+        return mapper.readTree(new String(Base64.getUrlDecoder().decode(disclosure), StandardCharsets.UTF_8));
+    }
+
+    private List<JsonNode> decodeAll(final SelectiveDisclosureClaimSetUtil util) throws Exception {
+        final List<JsonNode> decoded = new ArrayList<>();
+        for (final String disclosure : util.getFormattedDisclosures().split("~")) {
+            decoded.add(decode(disclosure));
+        }
+        return decoded;
+    }
+
+    private JsonNode valueOf(final SelectiveDisclosureClaimSetUtil util, final String name) throws Exception {
+        for (final JsonNode disclosure : decodeAll(util)) {
+            if (name.equals(disclosure.get(1).asText())) {
+                return disclosure.get(2);
+            }
+        }
+        Assert.fail("No disclosure for claim " + name);
+        return null;
+    }
+
+    @Test
+    public void testDisclosureIsArrayOfSaltNameAndValue() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("given_name", "John"));
+
+        final List<JsonNode> disclosures = decodeAll(util);
+        Assert.assertEquals(disclosures.size(), 1);
+
+        final JsonNode disclosure = disclosures.get(0);
+        Assert.assertTrue(disclosure.isArray());
+        Assert.assertEquals(disclosure.size(), 3);
+        Assert.assertTrue(disclosure.get(0).isTextual());
+        Assert.assertFalse(disclosure.get(0).asText().isEmpty());
+        Assert.assertEquals(disclosure.get(1).asText(), "given_name");
+        Assert.assertEquals(disclosure.get(2).asText(), "John");
+    }
+
+    @Test
+    public void testStringValueKeepsType() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("given_name", "John"));
+
+        final JsonNode value = valueOf(util, "given_name");
+        Assert.assertTrue(value.isTextual());
+        Assert.assertEquals(value.asText(), "John");
+    }
+
+    @Test
+    public void testBooleanValueKeepsType() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("is_over_18", Boolean.TRUE));
+
+        final JsonNode value = valueOf(util, "is_over_18");
+        Assert.assertTrue(value.isBoolean());
+        Assert.assertTrue(value.booleanValue());
+    }
+
+    @Test
+    public void testNumberValueKeepsType() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("age", Integer.valueOf(42)));
+
+        final JsonNode value = valueOf(util, "age");
+        Assert.assertTrue(value.isNumber());
+        Assert.assertEquals(value.intValue(), 42);
+    }
 
-    @BeforeMethod
-    protected void setUp() throws Exception {
-        
-        Map<String, Object> claims = new HashMap<String, Object> ();
+    @Test
+    public void testObjectValueKeepsType() throws Exception {
+        final Map<String, String> address = new HashMap<>();
+        address.put("street_address", "123 Main St");
+        address.put("locality", "Anytown");
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("address", address));
+
+        final JsonNode value = valueOf(util, "address");
+        Assert.assertTrue(value.isObject());
+        Assert.assertEquals(value.get("street_address").asText(), "123 Main St");
+        Assert.assertEquals(value.get("locality").asText(), "Anytown");
+    }
+
+    @Test
+    public void testArrayValueKeepsType() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("nationalities", List.of("FI", "SE")));
+
+        final JsonNode value = valueOf(util, "nationalities");
+        Assert.assertTrue(value.isArray());
+        Assert.assertEquals(value.size(), 2);
+        Assert.assertEquals(value.get(0).asText(), "FI");
+        Assert.assertEquals(value.get(1).asText(), "SE");
+    }
+
+    @Test
+    public void testValueWithJsonSyntaxIsEscaped() throws Exception {
+        final String awkward = "O'Brien \"Bob\" \\ back\nnewline\ttab";
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("nickname", awkward));
+
+        final JsonNode value = valueOf(util, "nickname");
+        Assert.assertTrue(value.isTextual());
+        Assert.assertEquals(value.asText(), awkward);
+    }
+
+    @Test
+    public void testNameWithJsonSyntaxIsEscaped() throws Exception {
+        final String awkward = "od\"d\\name";
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of(awkward, "value"));
+
+        final JsonNode disclosure = decodeAll(util).get(0);
+        Assert.assertEquals(disclosure.get(1).asText(), awkward);
+        Assert.assertEquals(disclosure.get(2).asText(), "value");
+    }
+
+    @Test
+    public void testDigestMatchesDisclosure() throws Exception {
+        final Map<String, Object> claims = new HashMap<>();
+        claims.put("given_name", "John");
+        claims.put("family_name", "Doe");
+        claims.put("is_over_18", Boolean.TRUE);
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(claims);
+
+        final MessageDigest digest = MessageDigest.getInstance("SHA-256");
+        for (final String disclosure : util.getFormattedDisclosures().split("~")) {
+            final String expected = Base64.getUrlEncoder().withoutPadding()
+                    .encodeToString(digest.digest(disclosure.getBytes(StandardCharsets.UTF_8)));
+            Assert.assertTrue(util.get_sd().contains(expected), "Missing digest of disclosure " + disclosure);
+        }
+        Assert.assertEquals(util.get_sd().size(), 3);
+    }
+
+    @Test
+    public void testDigestsAreSorted() {
+        final Map<String, Object> claims = new HashMap<>();
         claims.put("given_name", "John");
         claims.put("family_name", "Doe");
         claims.put("email", "johndoe at example.com");
         claims.put("phone_number", "+1-202-555-0101");
-        Map<String, String> address = new HashMap<String, String>();
-        address.put("street_address", "123 Main St");
-        address.put("locality", "Anytown");
-        address.put("street_address", "123 Main St");
-        address.put("region", "Anystate");
-        address.put("country", "US");
-        claims.put("address", address);
         claims.put("birthdate", "1940-01-01");
-        claims.put("is_over_18", true);
-        claims.put("is_over_21", true);
-        claims.put("is_over_65", true);
-        claimSet = new SelectiveDisclosureClaimSetUtil(claims);
-  
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(claims);
+
+        final List<String> sorted = new ArrayList<>(util.get_sd());
+        sorted.sort(null);
+        Assert.assertEquals(util.get_sd(), sorted);
+    }
+
+    @Test
+    public void testSaltsAreUnique() throws Exception {
+        final Map<String, Object> claims = new HashMap<>();
+        claims.put("given_name", "John");
+        claims.put("family_name", "Doe");
+        claims.put("email", "johndoe at example.com");
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(claims);
+
+        final Set<String> salts = new HashSet<>();
+        for (final JsonNode disclosure : decodeAll(util)) {
+            salts.add(disclosure.get(0).asText());
+        }
+        Assert.assertEquals(salts.size(), 3);
     }
 
     @Test
-    public void testBasicAuthSuccess() throws ParseException, NoSuchAlgorithmException, java.text.ParseException {
+    public void testSameClaimGetsDifferentSaltEachTime() throws Exception {
+        final String first = decodeAll(
+                new SelectiveDisclosureClaimSetUtil(Map.<String, Object>of("given_name", "John"))).get(0).get(0)
+                .asText();
+        final String second = decodeAll(
+                new SelectiveDisclosureClaimSetUtil(Map.<String, Object>of("given_name", "John"))).get(0).get(0)
+                .asText();
+        Assert.assertNotEquals(first, second);
+    }
 
-        
-        //Assert.assertEquals(claimSet.getFormattedDisclosures(), "");
-        ECKey ecJWK = ECKey.parse("{\"kty\":\"EC\",\"d\":\"_C4BcEXyIbnNY1njFip-e4yjO4U1hxOHuQ_tHJp8FlM\",\"crv\":\"P-256\",\"x\":\"qQ9ESeIrQ36JijWM-8xdcjXwY46RW3p9YDtP0MVaLnE\",\"y\":\"5yVZRqwp1tde_CnKC662wW-XFQhEOrGAPi0LTG4OpI8\"}");
-        
+    @Test
+    public void testDisclosureCountMatchesClaimCount() {
+        final Map<String, Object> claims = new HashMap<>();
+        claims.put("given_name", "John");
+        claims.put("family_name", "Doe");
+        claims.put("email", "johndoe at example.com");
+        claims.put("phone_number", "+1-202-555-0101");
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(claims);
+
+        Assert.assertEquals(util.getFormattedDisclosures().split("~").length, 4);
+        Assert.assertEquals(util.get_sd().size(), 4);
+    }
+
+    @Test
+    public void testDefaultAlgorithm() {
+        Assert.assertEquals(new SelectiveDisclosureClaimSetUtil(Map.<String, Object>of("given_name", "John")).get_alg(),
+                "sha-256");
+    }
+
+    @Test
+    public void testAlgorithmIsLowerCased() {
+        Assert.assertEquals(
+                new SelectiveDisclosureClaimSetUtil(Map.<String, Object>of("given_name", "John"), "SHA-512").get_alg(),
+                "sha-512");
+    }
+
+    @Test
+    public void testConfiguredAlgorithmIsUsedForDigest() throws Exception {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(
+                Map.<String, Object>of("given_name", "John"), "sha-512");
+
+        final String disclosure = util.getFormattedDisclosures();
+        final String expected = Base64.getUrlEncoder().withoutPadding().encodeToString(
+                MessageDigest.getInstance("SHA-512").digest(disclosure.getBytes(StandardCharsets.UTF_8)));
+        Assert.assertEquals(util.get_sd().get(0), expected);
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testUnsupportedAlgorithmRejected() {
+        new SelectiveDisclosureClaimSetUtil(Map.<String, Object>of("given_name", "John"), "sha-999");
+    }
+
+    @Test
+    public void testNoClaims() {
+        final SelectiveDisclosureClaimSetUtil util = new SelectiveDisclosureClaimSetUtil(Map.of());
+        Assert.assertTrue(util.get_sd().isEmpty());
+        Assert.assertEquals(util.getFormattedDisclosures(), "");
     }
 }
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTreeTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTreeTest.java
new file mode 100644
index 0000000..afcaf0f
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/util/SelectiveDisclosureClaimTreeTest.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.util;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedClaim;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class SelectiveDisclosureClaimTreeTest {
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    private CredentialOfferRequestedClaim claim(final Object value, final String... path) throws Exception {
+        final Map<String, Object> map = new HashMap<>();
+        map.put("path", List.of(path));
+        map.put("value", value);
+        return CredentialOfferRequestedClaim.parse(map);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> object(final Map<String, Object> parent, final String name) {
+        final Object child = parent.get(name);
+        Assert.assertTrue(child instanceof Map, "Expected object at " + name);
+        return (Map<String, Object>) child;
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<String> sd(final Map<String, Object> object) {
+        final Object digests = object.get("_sd");
+        Assert.assertTrue(digests instanceof List, "Expected _sd digests");
+        return (List<String>) digests;
+    }
+
+    private Set<String> disclosedNames(final SelectiveDisclosureClaimTree tree) throws Exception {
+        final Set<String> names = new HashSet<>();
+        for (final String disclosure : tree.getFormattedDisclosures().split("~")) {
+            final JsonNode decoded =
+                    mapper.readTree(new String(Base64.getUrlDecoder().decode(disclosure), StandardCharsets.UTF_8));
+            names.add(decoded.get(1).asText());
+        }
+        return names;
+    }
+
+    @Test
+    public void testFlatClaimsAreDisclosedAtRoot() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(
+                List.of(claim("John", "given_name"), claim("Doe", "family_name")));
+
+        Assert.assertEquals(tree.getClaims().keySet(), Set.of("_sd"));
+        Assert.assertEquals(sd(tree.getClaims()).size(), 2);
+        Assert.assertEquals(disclosedNames(tree), Set.of("given_name", "family_name"));
+    }
+
+    @Test
+    public void testLeafValuesAreNotInTheTree() throws Exception {
+        final SelectiveDisclosureClaimTree tree =
+                new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name")));
+
+        Assert.assertFalse(tree.getClaims().containsKey("given_name"));
+    }
+
+    @Test
+    public void testNestedClaimsShareOneObject() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(
+                List.of(claim("Anytown", "address", "locality"), claim("US", "address", "country")));
+
+        Assert.assertEquals(tree.getClaims().keySet(), Set.of("address"));
+        Assert.assertEquals(sd(object(tree.getClaims(), "address")).size(), 2);
+        Assert.assertEquals(disclosedNames(tree), Set.of("locality", "country"));
+    }
+
+    @Test
+    public void testFlatAndNestedClaimsCoexist() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name"),
+                claim("Anytown", "address", "locality"), claim("US", "address", "country")));
+
+        Assert.assertEquals(tree.getClaims().keySet(), Set.of("_sd", "address"));
+        Assert.assertEquals(sd(tree.getClaims()).size(), 1);
+        Assert.assertEquals(sd(object(tree.getClaims(), "address")).size(), 2);
+        Assert.assertEquals(disclosedNames(tree), Set.of("given_name", "locality", "country"));
+    }
+
+    @Test
+    public void testDeepNesting() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(
+                List.of(claim(1, "a", "b", "c"), claim(2, "a", "b", "d"), claim(3, "a", "e")));
+
+        final Map<String, Object> a = object(tree.getClaims(), "a");
+        Assert.assertEquals(a.keySet(), Set.of("b", "_sd"));
+        Assert.assertEquals(sd(a).size(), 1);
+        Assert.assertEquals(sd(object(a, "b")).size(), 2);
+        Assert.assertEquals(disclosedNames(tree), Set.of("c", "d", "e"));
+    }
+
+    @Test
+    public void testSeparateBranches() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(
+                List.of(claim("Anytown", "address", "locality"), claim("Suomi", "birth_place", "country")));
+
+        Assert.assertEquals(tree.getClaims().keySet(), Set.of("address", "birth_place"));
+        Assert.assertEquals(sd(object(tree.getClaims(), "address")).size(), 1);
+        Assert.assertEquals(sd(object(tree.getClaims(), "birth_place")).size(), 1);
+    }
+
+    @Test
+    public void testDisclosureCountMatchesClaimCount() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name"),
+                claim("Anytown", "address", "locality"), claim("US", "address", "country"),
+                claim(1, "a", "b", "c")));
+
+        Assert.assertEquals(tree.getFormattedDisclosures().split("~").length, 4);
+    }
+
+    @Test
+    public void testValueTypesSurvive() throws Exception {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(
+                List.of(claim(Boolean.TRUE, "address", "verified"), claim(Integer.valueOf(42), "address", "floor")));
+
+        final List<JsonNode> decoded = new ArrayList<>();
+        for (final String disclosure : tree.getFormattedDisclosures().split("~")) {
+            decoded.add(mapper.readTree(new String(Base64.getUrlDecoder().decode(disclosure), StandardCharsets.UTF_8)));
+        }
+        for (final JsonNode disclosure : decoded) {
+            if ("verified".equals(disclosure.get(1).asText())) {
+                Assert.assertTrue(disclosure.get(2).isBoolean());
+                Assert.assertTrue(disclosure.get(2).booleanValue());
+            } else {
+                Assert.assertTrue(disclosure.get(2).isNumber());
+                Assert.assertEquals(disclosure.get(2).intValue(), 42);
+            }
+        }
+        Assert.assertEquals(decoded.size(), 2);
+    }
+
+    @Test
+    public void testAlgorithm() throws Exception {
+        Assert.assertEquals(new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name"))).getAlgorithm(),
+                "sha-256");
+        Assert.assertEquals(
+                new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name")), "SHA-512").getAlgorithm(),
+                "sha-512");
+    }
+
+    @Test
+    public void testNoClaims() {
+        final SelectiveDisclosureClaimTree tree = new SelectiveDisclosureClaimTree(List.of());
+        Assert.assertTrue(tree.getClaims().isEmpty());
+        Assert.assertEquals(tree.getFormattedDisclosures(), "");
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testSameClaimTwiceRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim("John", "given_name"), claim("Jack", "given_name")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testSameNestedClaimTwiceRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(
+                List.of(claim("Anytown", "address", "locality"), claim("Othertown", "address", "locality")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testClaimThatIsAlsoObjectRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim("whole thing", "address"), claim("Anytown", "address",
+                "locality")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testClaimThatIsAlsoObjectRejectedInReverseOrder() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim("Anytown", "address", "locality"), claim("whole thing",
+                "address")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testDeepClaimThatIsAlsoObjectRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim(1, "a", "b"), claim(2, "a", "b", "c")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testReservedLeafNameRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim("value", "_sd")));
+    }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testReservedObjectNameRejected() throws Exception {
+        new SelectiveDisclosureClaimTree(List.of(claim("value", "address", "_sd", "locality")));
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list