From 2d993b6f67d46d6ffb9b453db188f9ad423a7e6b Mon Sep 17 00:00:00 2001 From: Francis Pouatcha Date: Thu, 4 Apr 2024 12:26:45 +0100 Subject: [PATCH] initial clone from keycloak-core --- .gitignore | 13 + .mvn/wrapper/MavenWrapperDownloader.java | 117 +++++++ .mvn/wrapper/maven-wrapper.properties | 2 + mvnw | 310 ++++++++++++++++++ mvnw.cmd | 182 ++++++++++ pom.xml | 34 ++ .../adorsys/ssi/sdjwt/AbstractSdJwtClaim.java | 24 ++ .../adorsys/ssi/sdjwt/ArrayDisclosure.java | 111 +++++++ .../adorsys/ssi/sdjwt/DecoyArrayElement.java | 50 +++ .../com/adorsys/ssi/sdjwt/DecoyClaim.java | 31 ++ .../com/adorsys/ssi/sdjwt/DecoyEntry.java | 28 ++ .../com/adorsys/ssi/sdjwt/Disclosable.java | 62 ++++ .../adorsys/ssi/sdjwt/DisclosureRedList.java | 38 +++ .../com/adorsys/ssi/sdjwt/DisclosureSpec.java | 176 ++++++++++ .../adorsys/ssi/sdjwt/IssuerSignedJWT.java | 184 +++++++++++ .../java/com/adorsys/ssi/sdjwt/SdJws.java | 95 ++++++ .../java/com/adorsys/ssi/sdjwt/SdJwt.java | 235 +++++++++++++ .../adorsys/ssi/sdjwt/SdJwtArrayElement.java | 19 ++ .../com/adorsys/ssi/sdjwt/SdJwtClaim.java | 23 ++ .../com/adorsys/ssi/sdjwt/SdJwtClaimName.java | 39 +++ .../java/com/adorsys/ssi/sdjwt/SdJwtSalt.java | 32 ++ .../com/adorsys/ssi/sdjwt/SdJwtUtils.java | 92 ++++++ .../ssi/sdjwt/UndisclosedArrayElement.java | 54 +++ .../adorsys/ssi/sdjwt/UndisclosedClaim.java | 83 +++++ .../ssi/sdjwt/VisibleArrayElement.java | 26 ++ .../adorsys/ssi/sdjwt/VisibleSdJwtClaim.java | 58 ++++ .../adorsys/ssi/sdjwt/vp/KeyBindingJWT.java | 32 ++ .../com/adorsys/ssi/sdjwt/vp/SdJwtVP.java | 250 ++++++++++++++ .../ssi/sdjwt/ArrayElementDisclosureTest.java | 65 ++++ .../sdjwt/ArrayElementSerializationTest.java | 44 +++ .../ssi/sdjwt/DisclosureRedListTest.java | 52 +++ .../ssi/sdjwt/IssuerSignedJWTTest.java | 120 +++++++ .../adorsys/ssi/sdjwt/JsonClaimsetTest.java | 63 ++++ .../ssi/sdjwt/JsonNodeComparisonTest.java | 23 ++ .../adorsys/ssi/sdjwt/SdJWTSamplesTest.java | 162 +++++++++ .../java/com/adorsys/ssi/sdjwt/SdJwtTest.java | 92 ++++++ .../com/adorsys/ssi/sdjwt/SdJwtUtilsTest.java | 110 +++++++ .../com/adorsys/ssi/sdjwt/TestSettings.java | 205 ++++++++++++ .../java/com/adorsys/ssi/sdjwt/TestUtils.java | 48 +++ .../ssi/sdjwt/UndisclosedClaimTest.java | 45 +++ .../ssi/sdjwt/sdjwtvp/SdJwtVPTest.java | 178 ++++++++++ .../ssi/sdjwt/sdjwtvp/TestCompareSdJwt.java | 101 ++++++ .../sdjwt/a1.example2-address-payload.json | 12 + .../sdjwt/a1.example2-holder-claims.json | 14 + .../sdjwt/a1.example2-issuer-claims.json | 5 + .../sdjwt/a1.example2-issuer-payload.json | 28 ++ .../resources/sdjwt/a1.example2-sdjwt.txt | 1 + .../resources/sdjwt/s3.3-holder-claims.json | 17 + .../resources/sdjwt/s3.3-issuer-claims.json | 14 + .../resources/sdjwt/s3.3-issuer-payload.json | 26 ++ .../resources/sdjwt/s3.3-unsecured-sd-jwt.txt | 28 ++ .../resources/sdjwt/s6.1-holder-claims.json | 20 ++ .../resources/sdjwt/s6.1-issued-payload.txt | 29 ++ ...1-issuer-payload-decoy-array-ellement.json | 19 ++ ...uer-payload-udisclosed-array-ellement.json | 18 + .../resources/sdjwt/s6.1-issuer-payload.json | 18 + .../sdjwt/s6.2-key-binding-claims.json | 5 + .../sdjwt/s6.2-presented-sdjwtvp.txt | 23 ++ .../resources/sdjwt/s7-holder-claims.json | 9 + .../resources/sdjwt/s7-issuer-claims.json | 5 + .../resources/sdjwt/s7.1-issuer-payload.json | 8 + .../resources/sdjwt/s7.2-issuer-payload.json | 15 + .../resources/sdjwt/s7.2b-issuer-payload.json | 15 + .../resources/sdjwt/s7.3-issuer-payload.json | 8 + src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt | 1 + src/test/resources/sdjwt/s7.3-sdjwt.txt | 1 + src/test/resources/sdjwt/test-settings.json | 29 ++ src/test/resources/sdjwt/test-settings.yml | 32 ++ 68 files changed, 4108 insertions(+) create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/AbstractSdJwtClaim.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/ArrayDisclosure.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/DecoyArrayElement.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/DecoyClaim.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/DecoyEntry.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/Disclosable.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/DisclosureRedList.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/DisclosureSpec.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/IssuerSignedJWT.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJws.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwt.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwtArrayElement.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaim.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaimName.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwtSalt.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/SdJwtUtils.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/UndisclosedArrayElement.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/UndisclosedClaim.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/VisibleArrayElement.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/VisibleSdJwtClaim.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/vp/KeyBindingJWT.java create mode 100644 src/main/java/com/adorsys/ssi/sdjwt/vp/SdJwtVP.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/ArrayElementDisclosureTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/ArrayElementSerializationTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/DisclosureRedListTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/IssuerSignedJWTTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/JsonClaimsetTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/JsonNodeComparisonTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/SdJWTSamplesTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/SdJwtTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/SdJwtUtilsTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/TestSettings.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/TestUtils.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/UndisclosedClaimTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/SdJwtVPTest.java create mode 100644 src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/TestCompareSdJwt.java create mode 100644 src/test/resources/sdjwt/a1.example2-address-payload.json create mode 100644 src/test/resources/sdjwt/a1.example2-holder-claims.json create mode 100644 src/test/resources/sdjwt/a1.example2-issuer-claims.json create mode 100644 src/test/resources/sdjwt/a1.example2-issuer-payload.json create mode 100644 src/test/resources/sdjwt/a1.example2-sdjwt.txt create mode 100644 src/test/resources/sdjwt/s3.3-holder-claims.json create mode 100644 src/test/resources/sdjwt/s3.3-issuer-claims.json create mode 100644 src/test/resources/sdjwt/s3.3-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt create mode 100644 src/test/resources/sdjwt/s6.1-holder-claims.json create mode 100644 src/test/resources/sdjwt/s6.1-issued-payload.txt create mode 100644 src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json create mode 100644 src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json create mode 100644 src/test/resources/sdjwt/s6.1-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s6.2-key-binding-claims.json create mode 100644 src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt create mode 100644 src/test/resources/sdjwt/s7-holder-claims.json create mode 100644 src/test/resources/sdjwt/s7-issuer-claims.json create mode 100644 src/test/resources/sdjwt/s7.1-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s7.2-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s7.2b-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s7.3-issuer-payload.json create mode 100644 src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt create mode 100644 src/test/resources/sdjwt/s7.3-sdjwt.txt create mode 100644 src/test/resources/sdjwt/test-settings.json create mode 100644 src/test/resources/sdjwt/test-settings.yml diff --git a/.gitignore b/.gitignore index 524f096..ff056a5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,16 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# OS stuff +################### +.DS_Store + +# Intellij +################### +.idea +*.iml +!.idea/icon.png + +# mvn +target \ No newline at end of file diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..b901097 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * 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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..642d572 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..41c0f0c --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..8611571 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..da3d142 --- /dev/null +++ b/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + com.adorsys.ssi + sd-jwt + jar + 1.0-SNAPSHOT + sd-jwt + http://maven.apache.org + + 11 + 11 + + + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + + org.keycloak + keycloak-core + 24.0.2 + + + + junit + junit + 4.13.2 + test + + + diff --git a/src/main/java/com/adorsys/ssi/sdjwt/AbstractSdJwtClaim.java b/src/main/java/com/adorsys/ssi/sdjwt/AbstractSdJwtClaim.java new file mode 100644 index 0000000..73e9f4b --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/AbstractSdJwtClaim.java @@ -0,0 +1,24 @@ + +package com.adorsys.ssi.sdjwt; + +/** + * @author Francis Pouatcha + * + */ +public abstract class AbstractSdJwtClaim implements SdJwtClaim { + private final SdJwtClaimName claimName; + + public AbstractSdJwtClaim(SdJwtClaimName claimName) { + this.claimName = claimName; + } + + @Override + public SdJwtClaimName getClaimName() { + return claimName; + } + + @Override + public String getClaimNameAsString() { + return claimName.toString(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/ArrayDisclosure.java b/src/main/java/com/adorsys/ssi/sdjwt/ArrayDisclosure.java new file mode 100644 index 0000000..c03f9dc --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/ArrayDisclosure.java @@ -0,0 +1,111 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * Handles selective disclosure of elements within a top-level array claim, + * supporting both visible and undisclosed elements. + * + * @author Francis Pouatcha + * + */ +public class ArrayDisclosure extends AbstractSdJwtClaim { + private final List elements; + private JsonNode visibleClaimValue = null; + private final List decoyElements; + + private ArrayDisclosure(SdJwtClaimName claimName, List elements, + List decoyElements) { + super(claimName); + this.elements = elements; + this.decoyElements = decoyElements; + } + + /** + * Print the array with visible and invisible elements. + */ + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + if (visibleClaimValue != null) + return visibleClaimValue; + + List visibleElts = new ArrayList<>(); + elements.stream() + .filter(Objects::nonNull) + .forEach(e -> visibleElts.add(e.getVisibleValue(hashAlgo))); + + decoyElements.stream() + .filter(Objects::nonNull) + .forEach(e -> { + if (e.getIndex() < visibleElts.size()) + visibleElts.add(e.getIndex(), e.getVisibleValue(hashAlgo)); + else + visibleElts.add(e.getVisibleValue(hashAlgo)); + }); + + final ArrayNode n = SdJwtUtils.mapper.createArrayNode(); + visibleElts.forEach(n::add); + visibleClaimValue = n; + return visibleClaimValue; + } + + @Override + public List getDisclosureStrings() { + final List disclosureStrings = new ArrayList<>(); + elements.stream() + .filter(Objects::nonNull) + .forEach(e -> { + String disclosureString = e.getDisclosureString(); + if (disclosureString != null) + disclosureStrings.add(disclosureString); + }); + return disclosureStrings; + } + + public static class Builder { + private SdJwtClaimName claimName; + private final List elements = new ArrayList<>(); + private final List decoyElements = new ArrayList<>(); + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withVisibleElement(JsonNode elementValue) { + this.elements.add(new VisibleArrayElement(elementValue)); + return this; + } + + public Builder withUndisclosedElement(SdJwtSalt salt, JsonNode elementValue) { + SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + this.elements.add(UndisclosedArrayElement.builder() + .withSalt(sdJwtSalt) + .withArrayElement(elementValue) + .build()); + return this; + } + + public void withDecoyElt(Integer position, SdJwtSalt salt) { + SdJwtSalt sdJwtSalt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + DecoyArrayElement decoyElement = DecoyArrayElement.builder().withSalt(sdJwtSalt).atIndex(position).build(); + this.decoyElements.add(decoyElement); + } + + public ArrayDisclosure build() { + return new ArrayDisclosure(claimName, Collections.unmodifiableList(elements), + Collections.unmodifiableList(decoyElements)); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/DecoyArrayElement.java b/src/main/java/com/adorsys/ssi/sdjwt/DecoyArrayElement.java new file mode 100644 index 0000000..1233abd --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/DecoyArrayElement.java @@ -0,0 +1,50 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class DecoyArrayElement extends DecoyEntry { + + private final Integer index; + + private DecoyArrayElement(SdJwtSalt salt, Integer index) { + super(salt); + this.index = index; + } + + public JsonNode getVisibleValue(String hashAlg) { + return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg)); + } + + public Integer getIndex() { + return index; + } + + public static class Builder { + private SdJwtSalt salt; + private Integer index; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder atIndex(Integer index) { + this.index = index; + return this; + } + + public DecoyArrayElement build() { + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new DecoyArrayElement(salt, index); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/DecoyClaim.java b/src/main/java/com/adorsys/ssi/sdjwt/DecoyClaim.java new file mode 100644 index 0000000..7d978c5 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/DecoyClaim.java @@ -0,0 +1,31 @@ + +package com.adorsys.ssi.sdjwt; + +/** + * + * @author Francis Pouatcha + */ +public class DecoyClaim extends DecoyEntry { + + private DecoyClaim(SdJwtSalt salt) { + super(salt); + } + + public static class Builder { + private SdJwtSalt salt; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public DecoyClaim build() { + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new DecoyClaim(salt); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/DecoyEntry.java b/src/main/java/com/adorsys/ssi/sdjwt/DecoyEntry.java new file mode 100644 index 0000000..49a2b98 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/DecoyEntry.java @@ -0,0 +1,28 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Objects; + +import org.keycloak.jose.jws.crypto.HashUtils; + +/** + * Handles hash production for a decoy entry from the given salt. + * + * @author Francis Pouatcha + * + */ +public abstract class DecoyEntry { + private final SdJwtSalt salt; + + protected DecoyEntry(SdJwtSalt salt) { + this.salt = Objects.requireNonNull(salt, "DecoyEntry always requires a non null salt"); + } + + public SdJwtSalt getSalt() { + return salt; + } + + public String getDisclosureDigest(String hashAlg) { + return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, salt.toString().getBytes())); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/Disclosable.java b/src/main/java/com/adorsys/ssi/sdjwt/Disclosable.java new file mode 100644 index 0000000..7294663 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/Disclosable.java @@ -0,0 +1,62 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Objects; + +import org.keycloak.jose.jws.crypto.HashUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Handles undisclosed claims and array elements, providing functionality + * to generate disclosure digests from Base64Url encoded strings. + * + * Hiding claims and array elements occurs by including their digests + * instead of plaintext in the signed verifiable credential. + * + * @author Francis Pouatcha + * + */ +public abstract class Disclosable { + private final SdJwtSalt salt; + + /** + * Returns the array of undisclosed value, for + * encoding (disclosure string) and hashing (_sd digest array in the VC). + */ + abstract Object[] toArray(); + + protected Disclosable(SdJwtSalt salt) { + this.salt = Objects.requireNonNull(salt, "Disclosure always requires a salt must not be null"); + } + + public SdJwtSalt getSalt() { + return salt; + } + + public String getSaltAsString() { + return salt.toString(); + } + + public String toJson() { + try { + return SdJwtUtils.printJsonArray(toArray()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public String getDisclosureString() { + String json = toJson(); + return SdJwtUtils.encodeNoPad(json.getBytes()); + } + + public String getDisclosureDigest(String hashAlg) { + return SdJwtUtils.encodeNoPad(HashUtils.hash(hashAlg, getDisclosureString().getBytes())); + } + + @Override + public String toString() { + return getDisclosureString(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/DisclosureRedList.java b/src/main/java/com/adorsys/ssi/sdjwt/DisclosureRedList.java new file mode 100644 index 0000000..c4ae3cb --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/DisclosureRedList.java @@ -0,0 +1,38 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class DisclosureRedList { + public static final List redList = Collections + .unmodifiableList(Arrays.asList("iss", "iat", "nbf", "exp", "cnf", "vct", "status")); + + private final Set redListClaimNames; + public static final DisclosureRedList defaultList = defaultList(); + + public DisclosureRedList of(Set redListClaimNames) { + return new DisclosureRedList(redListClaimNames); + } + + private static DisclosureRedList defaultList() { + return new DisclosureRedList(redList.stream().map(SdJwtClaimName::of).collect(Collectors.toSet())); + } + + private DisclosureRedList(Set redListClaimNames) { + this.redListClaimNames = Collections.unmodifiableSet(redListClaimNames); + } + + public boolean isRedListedClaimName(SdJwtClaimName claimName) { + return redListClaimNames.contains(claimName); + } + + public boolean containsRedListedClaimNames(Collection claimNames) { + return !redListClaimNames.isEmpty() && !claimNames.isEmpty() + && !Collections.disjoint(redListClaimNames, claimNames); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/DisclosureSpec.java b/src/main/java/com/adorsys/ssi/sdjwt/DisclosureSpec.java new file mode 100644 index 0000000..d9a0ab9 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/DisclosureSpec.java @@ -0,0 +1,176 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages the specification of undisclosed claims and array elements. + * + * @author Francis Pouatcha + * + */ +public class DisclosureSpec { + + // Map of undisclosed claims and corresponding salt. + // salt can be null; + private final Map undisclosedClaims; + + // List of decoy claim. Digest will be produced from disclosure data (salt) + private final List decoyClaims; + + // Key is the claim name, value is the list of undisclosed elements + private final Map> undisclosedArrayElts; + + // Key is the claim name, value is the list of decoy elements + // Digest will be produced from disclosure data (salt) + private final Map> decoyArrayElts; + + private DisclosureSpec(Map undisclosedClaims, + List decoyClaims, + Map> undisclosedArrayElts, + Map> decoyArrayElts) { + this.undisclosedClaims = undisclosedClaims; + this.decoyClaims = decoyClaims; + this.undisclosedArrayElts = undisclosedArrayElts; + this.decoyArrayElts = decoyArrayElts; + } + + public Map getUndisclosedArrayElts(SdJwtClaimName arrayClaimName) { + return undisclosedArrayElts.get(arrayClaimName); + } + + public Map getDecoyArrayElts(SdJwtClaimName arrayClaimName) { + return decoyArrayElts.get(arrayClaimName); + } + + public Map getUndisclosedClaims() { + return undisclosedClaims; + } + + public List getDecoyClaims() { + return decoyClaims; + } + + // check if a claim is undisclosed + public DisclosureData getUndisclosedClaim(SdJwtClaimName claimName) { + return undisclosedClaims.get(claimName); + } + + // test is claim has undisclosed array elements + public boolean hasUndisclosedArrayElts(SdJwtClaimName claimName) { + return undisclosedArrayElts.containsKey(claimName); + } + + public static class Builder { + private final Map undisclosedClaims = new HashMap<>(); + private final List decoyClaims = new ArrayList<>(); + private final Map> undisclosedArrayElts = new HashMap<>(); + private final Map> decoyArrayElts = new HashMap<>(); + private DisclosureRedList redListedClaimNames; + + public Builder withUndisclosedClaim(String claimName, String salt) { + this.undisclosedClaims.put(SdJwtClaimName.of(claimName), DisclosureData.of(salt)); + return this; + } + + public Builder withUndisclosedClaim(String claimName) { + return withUndisclosedClaim(claimName, null); + } + + public Builder withDecoyClaim(String salt) { + this.decoyClaims.add(DisclosureData.of(salt)); + return this; + } + + public Builder withUndisclosedArrayElt(String claimName, Integer undisclosedEltIndex, String salt) { + Map indexes = this.undisclosedArrayElts.computeIfAbsent( + SdJwtClaimName.of(claimName), + k -> new HashMap<>()); + indexes.put(undisclosedEltIndex, DisclosureData.of(salt)); + return this; + } + + public Builder withDecoyArrayElt(String claimName, Integer decoyEltIndex, String salt) { + Map indexes = this.decoyArrayElts.computeIfAbsent(SdJwtClaimName.of(claimName), + k -> new HashMap<>()); + + indexes.put(decoyEltIndex, DisclosureData.of(salt)); + return this; + } + + public Builder withRedListedClaimNames(DisclosureRedList redListedClaimNames) { + this.redListedClaimNames = redListedClaimNames; + return this; + } + + public DisclosureSpec build() { + // Validate redlist + validateRedList(); + + Map> undisclosedArrayEltMap = new HashMap<>(); + undisclosedArrayElts.forEach((k, v) -> { + undisclosedArrayEltMap.put(k, Collections.unmodifiableMap((v))); + }); + + Map> decoyArrayEltMap = new HashMap<>(); + decoyArrayElts.forEach((k, v) -> { + decoyArrayEltMap.put(k, Collections.unmodifiableMap((v))); + }); + + return new DisclosureSpec(Collections.unmodifiableMap(undisclosedClaims), + Collections.unmodifiableList(decoyClaims), + Collections.unmodifiableMap(undisclosedArrayEltMap), + Collections.unmodifiableMap(decoyArrayEltMap)); + } + + private void validateRedList() { + // Work with default if none set. + if (redListedClaimNames == null) { + redListedClaimNames = DisclosureRedList.defaultList; + } + + // Validate undisclosed claims + if (redListedClaimNames.containsRedListedClaimNames(undisclosedClaims.keySet())) { + throw new IllegalArgumentException("UndisclosedClaims contains red listed claim names"); + } + + // Validate undisclosed array claims + if (redListedClaimNames.containsRedListedClaimNames(undisclosedArrayElts.keySet())) { + throw new IllegalArgumentException("UndisclosedArrays with red listed claim names"); + } + + // Validate undisclosed claims + if (redListedClaimNames.containsRedListedClaimNames(decoyArrayElts.keySet())) { + throw new IllegalArgumentException("decoyArrayElts contains red listed claim names"); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class DisclosureData { + private final SdJwtSalt salt; + + private DisclosureData() { + this.salt = null; + } + + private DisclosureData(String salt) { + this.salt = salt == null ? null : SdJwtSalt.of(salt); + } + + public static DisclosureData of(String salt) { + return salt == null ? new DisclosureData() : new DisclosureData(salt); + } + + public SdJwtSalt getSalt() { + return salt; + } + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/IssuerSignedJWT.java b/src/main/java/com/adorsys/ssi/sdjwt/IssuerSignedJWT.java new file mode 100644 index 0000000..8ebe3ca --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/IssuerSignedJWT.java @@ -0,0 +1,184 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jws.JWSInput; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Handle verifiable credentials (SD-JWT VC), enabling the parsing + * of existing VCs as well as the creation and signing of new ones. + * It integrates with Keycloak's SignatureSignerContext to facilitate + * the generation of issuer signature. + * + * @author Francis Pouatcha + */ +public class IssuerSignedJWT extends SdJws { + + public static IssuerSignedJWT fromJws(String jwsString) { + return new IssuerSignedJWT(jwsString); + } + + public IssuerSignedJWT toSignedJWT(SignatureSignerContext signer, String jwsType) { + JWSInput jwsInput = sign(getPayload(), signer, jwsType); + return new IssuerSignedJWT(getPayload(), jwsInput); + } + + private IssuerSignedJWT(String jwsString) { + super(jwsString); + } + + private IssuerSignedJWT(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures) { + super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures)); + } + + private IssuerSignedJWT(JsonNode payload, JWSInput jwsInput) { + super(payload, jwsInput); + } + + private IssuerSignedJWT(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures, SignatureSignerContext signer, String jwsType) { + super(generatePayloadString(claims, decoyClaims, hashAlg, nestedDisclosures), signer, jwsType); + } + + /* + * Generates the payload of the issuer signed jwt from the list + * of claims. + */ + private static JsonNode generatePayloadString(List claims, List decoyClaims, String hashAlg, + boolean nestedDisclosures) { + + SdJwtUtils.requireNonEmpty(hashAlg, "hashAlg must not be null or empty"); + final List claimsInternal = claims == null ? Collections.emptyList() + : Collections.unmodifiableList(claims); + final List decoyClaimsInternal = decoyClaims == null ? Collections.emptyList() + : Collections.unmodifiableList(decoyClaims); + + try { + // Check no dupplicate claim names + claimsInternal.stream() + .filter(Objects::nonNull) + // is any duplicate, toMap will throw IllegalStateException + .collect(Collectors.toMap(SdJwtClaim::getClaimName, claim -> claim)); + } catch (IllegalStateException e) { + throw new IllegalArgumentException("claims must not contain duplicate claim names", e); + } + + ArrayNode sdArray = SdJwtUtils.mapper.createArrayNode(); + // first filter all UndisclosedClaim + // then sort by salt + // then push digest into the sdArray + List digests = claimsInternal.stream() + .filter(claim -> claim instanceof UndisclosedClaim) + .map(claim -> (UndisclosedClaim) claim) + .collect(Collectors.toMap(UndisclosedClaim::getSalt, claim -> claim)) + .entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(Map.Entry::getValue) + .filter(Objects::nonNull) + .map(od -> od.getDisclosureDigest(hashAlg)) + .collect(Collectors.toList()); + + // add decoy claims + decoyClaimsInternal.stream().map(claim -> claim.getDisclosureDigest(hashAlg)).forEach(digests::add); + + digests.stream().sorted().forEach(sdArray::add); + + ObjectNode payload = SdJwtUtils.mapper.createObjectNode(); + + if (sdArray.size() > 0) { + // drop _sd claim if empty + payload.set(CLAIM_NAME_SELECTIVE_DISCLOSURE, sdArray); + } + if (sdArray.size() > 0 || nestedDisclosures) { + // add sd alg only if ay disclosure. + payload.put(CLAIM_NAME_SD_HASH_ALGORITHM, hashAlg); + } + + // then put all other claims in the paypload + // Disclosure of array of elements is handled + // by the corresponding claim object. + claimsInternal.stream() + .filter(Objects::nonNull) + .filter(claim -> !(claim instanceof UndisclosedClaim)) + .forEach(nullableClaim -> { + SdJwtClaim claim = Objects.requireNonNull(nullableClaim); + payload.set(claim.getClaimNameAsString(), claim.getVisibleClaimValue(hashAlg)); + }); + + return payload; + } + + // SD-JWT Claims + public static final String CLAIM_NAME_SELECTIVE_DISCLOSURE = "_sd"; + public static final String CLAIM_NAME_SD_HASH_ALGORITHM = "_sd_alg"; + + // Builder + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private List claims; + private String hashAlg; + private SignatureSignerContext signer; + private List decoyClaims; + private boolean nestedDisclosures; + private String jwsType; + + public Builder withClaims(List claims) { + this.claims = claims; + return this; + } + + public Builder withDecoyClaims(List decoyClaims) { + this.decoyClaims = decoyClaims; + return this; + } + + public Builder withHashAlg(String hashAlg) { + this.hashAlg = hashAlg; + return this; + } + + public Builder withSigner(SignatureSignerContext signer) { + this.signer = signer; + return this; + } + + public Builder withNestedDisclosures(boolean nestedDisclosures) { + this.nestedDisclosures = nestedDisclosures; + return this; + } + + public Builder withJwsType(String jwsType) { + this.jwsType = jwsType; + return this; + } + + public IssuerSignedJWT build() { + // Preinitialize hashAlg to sha-256 if not provided + hashAlg = hashAlg == null ? "sha-256" : hashAlg; + jwsType = jwsType == null ? "vc+sd-jwt" : jwsType; + // send an empty lise if claims not set. + claims = claims == null ? Collections.emptyList() : claims; + decoyClaims = decoyClaims == null ? Collections.emptyList() : decoyClaims; + if (signer != null) { + return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures, signer, jwsType); + } else { + return new IssuerSignedJWT(claims, decoyClaims, hashAlg, nestedDisclosures); + } + } + } + +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJws.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJws.java new file mode 100644 index 0000000..4e65869 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJws.java @@ -0,0 +1,95 @@ + +package com.adorsys.ssi.sdjwt; + +import java.io.IOException; +import java.util.Objects; + +import org.keycloak.common.VerificationException; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.crypto.SignatureVerifierContext; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Handle jws, either the issuer jwt or the holder key binding jwt. + * + * @author Francis Pouatcha + * + */ +public class SdJws { + private final JWSInput jwsInput; + private final JsonNode payload; + + public String toJws() { + if (jwsInput == null) { + throw new IllegalStateException("JWS not yet signed"); + } + return jwsInput.getWireString(); + } + + public JsonNode getPayload() { + return payload; + } + + public String getJwsString() { + return jwsInput.getWireString(); + } + + // Constructor for unsigned JWS + protected SdJws(JsonNode payload) { + this.payload = payload; + this.jwsInput = null; + } + + // Constructor from jws string with all parts + protected SdJws(String jwsString) { + this.jwsInput = parse(jwsString); + this.payload = readPayload(jwsInput); + } + + // Constructor for signed JWS + protected SdJws(JsonNode payload, JWSInput jwsInput) { + this.payload = payload; + this.jwsInput = jwsInput; + } + + protected SdJws(JsonNode payload, SignatureSignerContext signer, String jwsType) { + this.payload = payload; + this.jwsInput = sign(payload, signer, jwsType); + } + + protected static JWSInput sign(JsonNode payload, SignatureSignerContext signer, String jwsType) { + String jwsString = new JWSBuilder().type(jwsType).jsonContent(payload).sign(signer); + return parse(jwsString); + } + + public void verifySignature(SignatureVerifierContext verifier) throws VerificationException { + Objects.requireNonNull(verifier, "verifier must not be null"); + try { + if (!verifier.verify(jwsInput.getEncodedSignatureInput().getBytes("UTF-8"), jwsInput.getSignature())) { + throw new VerificationException("Invalid jws signature"); + } + } catch (Exception e) { + throw new VerificationException(e); + } + } + + private static final JWSInput parse(String jwsString) { + try { + return new JWSInput(Objects.requireNonNull(jwsString, "jwsString must not be null")); + } catch (JWSInputException e) { + throw new RuntimeException(e); + } + } + + private static final JsonNode readPayload(JWSInput jwsInput) { + try { + return SdJwtUtils.mapper.readTree(jwsInput.getContent()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwt.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwt.java new file mode 100644 index 0000000..1fd6c82 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwt.java @@ -0,0 +1,235 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.keycloak.crypto.SignatureSignerContext; +import com.adorsys.ssi.sdjwt.vp.KeyBindingJWT; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Main entry class for selective disclosure jwt (SD-JWT). + * + * @author Francis Pouatcha + */ +public class SdJwt { + public static final String DELIMITER = "~"; + + private final IssuerSignedJWT issuerSignedJWT; + private final List claims; + private final List disclosures = new ArrayList<>(); + + private SdJwt(DisclosureSpec disclosureSpec, JsonNode claimSet, List nesteSdJwts, + Optional keyBindingJWT, + SignatureSignerContext signer, + String hashAlgorithm, + String jwsType) { + claims = new ArrayList<>(); + claimSet.fields() + .forEachRemaining(entry -> claims.add(createClaim(entry.getKey(), entry.getValue(), disclosureSpec))); + + this.issuerSignedJWT = IssuerSignedJWT.builder() + .withClaims(claims) + .withDecoyClaims(createdDecoyClaims(disclosureSpec)) + .withNestedDisclosures(!nesteSdJwts.isEmpty()) + .withSigner(signer) + .withHashAlg(hashAlgorithm) + .withJwsType(jwsType) + .build(); + + nesteSdJwts.stream().forEach(nestedJwt -> this.disclosures.addAll(nestedJwt.getDisclosures())); + this.disclosures.addAll(getDisclosureStrings(claims)); + } + + private Optional sdJwtString = Optional.empty(); + + private List createdDecoyClaims(DisclosureSpec disclosureSpec) { + return disclosureSpec.getDecoyClaims().stream() + .map(disclosureData -> DecoyClaim.builder().withSalt(disclosureData.getSalt()).build()) + .collect(Collectors.toList()); + } + + /** + * Prepare to a nested payload to this SD-JWT. + *

+ * droping the algo claim. + * + * @param nestedSdJwt + * @return + */ + public JsonNode asNestedPayload() { + JsonNode nestedPayload = issuerSignedJWT.getPayload(); + ((ObjectNode) nestedPayload).remove(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM); + return nestedPayload; + } + + public String toSdJwtString() { + List parts = new ArrayList<>(); + + parts.add(issuerSignedJWT.toJws()); + parts.addAll(disclosures); + parts.add(""); + + return String.join(DELIMITER, parts); + } + + private static List getDisclosureStrings(List claims) { + List disclosureStrings = new ArrayList<>(); + claims.stream() + .map(SdJwtClaim::getDisclosureStrings) + .forEach(disclosureStrings::addAll); + return Collections.unmodifiableList(disclosureStrings); + } + + @Override + public String toString() { + return sdJwtString.orElseGet(() -> { + String sdString = toSdJwtString(); + sdJwtString = Optional.of(sdString); + return sdString; + }); + } + + private SdJwtClaim createClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) { + DisclosureSpec.DisclosureData disclosureData = disclosureSpec.getUndisclosedClaim(SdJwtClaimName.of(claimName)); + + if (disclosureData != null) { + return createUndisclosedClaim(claimName, claimValue, disclosureData.getSalt()); + } else { + return createArrayOrVisibleClaim(claimName, claimValue, disclosureSpec); + } + } + + private SdJwtClaim createUndisclosedClaim(String claimName, JsonNode claimValue, SdJwtSalt salt) { + return UndisclosedClaim.builder() + .withClaimName(claimName) + .withClaimValue(claimValue) + .withSalt(salt) + .build(); + } + + private SdJwtClaim createArrayOrVisibleClaim(String claimName, JsonNode claimValue, DisclosureSpec disclosureSpec) { + SdJwtClaimName sdJwtClaimName = SdJwtClaimName.of(claimName); + Map undisclosedArrayElts = disclosureSpec + .getUndisclosedArrayElts(sdJwtClaimName); + Map decoyArrayElts = disclosureSpec.getDecoyArrayElts(sdJwtClaimName); + + if (undisclosedArrayElts != null || decoyArrayElts != null) { + return createArrayDisclosure(claimName, claimValue, undisclosedArrayElts, decoyArrayElts); + } else { + return VisibleSdJwtClaim.builder() + .withClaimName(claimName) + .withClaimValue(claimValue) + .build(); + } + } + + private SdJwtClaim createArrayDisclosure(String claimName, JsonNode claimValue, + Map undisclosedArrayElts, + Map decoyArrayElts) { + ArrayNode arrayNode = validateArrayNode(claimName, claimValue); + ArrayDisclosure.Builder arrayDisclosureBuilder = ArrayDisclosure.builder().withClaimName(claimName); + + if (undisclosedArrayElts != null) { + IntStream.range(0, arrayNode.size()) + .forEach(i -> processArrayElement(arrayDisclosureBuilder, arrayNode.get(i), + undisclosedArrayElts.get(i))); + } + + if (decoyArrayElts != null) { + decoyArrayElts.entrySet().stream() + .forEach(e -> arrayDisclosureBuilder.withDecoyElt(e.getKey(), e.getValue().getSalt())); + } + + return arrayDisclosureBuilder.build(); + } + + private ArrayNode validateArrayNode(String claimName, JsonNode claimValue) { + return Optional.of(claimValue) + .filter(v -> v.getNodeType() == JsonNodeType.ARRAY) + .map(v -> (ArrayNode) v) + .orElseThrow( + () -> new IllegalArgumentException("Expected array for claim with name: " + claimName)); + } + + private void processArrayElement(ArrayDisclosure.Builder builder, JsonNode elementValue, + DisclosureSpec.DisclosureData disclosureData) { + if (disclosureData != null) { + builder.withUndisclosedElement(disclosureData.getSalt(), elementValue); + } else { + builder.withVisibleElement(elementValue); + } + } + + public IssuerSignedJWT getIssuerSignedJWT() { + return issuerSignedJWT; + } + + public List getDisclosures() { + return disclosures; + } + + // builder for SdJwt + public static class Builder { + private DisclosureSpec disclosureSpec; + private JsonNode claimSet; + private Optional keyBindingJWT = Optional.empty(); + private SignatureSignerContext signer; + private final List nestedSdJwts = new ArrayList<>(); + private String hashAlgorithm; + private String jwsType; + + public Builder withDisclosureSpec(DisclosureSpec disclosureSpec) { + this.disclosureSpec = disclosureSpec; + return this; + } + + public Builder withClaimSet(JsonNode claimSet) { + this.claimSet = claimSet; + return this; + } + + public Builder withKeyBindingJWT(KeyBindingJWT keyBindingJWT) { + this.keyBindingJWT = Optional.of(keyBindingJWT); + return this; + } + + public Builder withSigner(SignatureSignerContext signer) { + this.signer = signer; + return this; + } + + public Builder withNestedSdJwt(SdJwt nestedSdJwt) { + nestedSdJwts.add(nestedSdJwt); + return this; + } + + public Builder withHashAlgorithm(String hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + return this; + } + + public Builder withJwsType(String jwsType) { + this.jwsType = jwsType; + return this; + } + + public SdJwt build() { + return new SdJwt(disclosureSpec, claimSet, nestedSdJwts, keyBindingJWT, signer, hashAlgorithm, jwsType); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwtArrayElement.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtArrayElement.java new file mode 100644 index 0000000..ceb924b --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtArrayElement.java @@ -0,0 +1,19 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + * + */ +public interface SdJwtArrayElement { + /** + * Returns the value visibly printed as array element + * in the issuer signed jwt. + */ + public JsonNode getVisibleValue(String hashAlg); + + public String getDisclosureString(); +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaim.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaim.java new file mode 100644 index 0000000..6fa4006 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaim.java @@ -0,0 +1,23 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Represents a top level claim in the payload of a JWT. + * + * @author Francis Pouatcha + */ +public interface SdJwtClaim { + + public SdJwtClaimName getClaimName(); + + public String getClaimNameAsString(); + + public JsonNode getVisibleClaimValue(String hashAlgo); + + public List getDisclosureStrings(); + +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaimName.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaimName.java new file mode 100644 index 0000000..7b372b8 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtClaimName.java @@ -0,0 +1,39 @@ + +package com.adorsys.ssi.sdjwt; + +/** + * Strong typing claim name to avoid parameter mismatch. + * + * Used as map key. Beware of the hashcode and equals implementation. + * + * @author Francis Pouatcha + */ +public class SdJwtClaimName { + private final String claimName; + + public SdJwtClaimName(String claimName) { + this.claimName = SdJwtUtils.requireNonEmpty(claimName, "claimName must not be empty"); + } + + public static SdJwtClaimName of(String claimName) { + return new SdJwtClaimName(claimName); + } + + @Override + public String toString() { + return claimName; + } + + @Override + public int hashCode() { + return claimName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SdJwtClaimName) { + return claimName.equals(((SdJwtClaimName) obj).claimName); + } + return false; + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwtSalt.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtSalt.java new file mode 100644 index 0000000..a9ef13d --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtSalt.java @@ -0,0 +1,32 @@ + +package com.adorsys.ssi.sdjwt; + +/** + * Strong typing salt to avoid parameter mismatch. + * + * Comparable to allow sorting in SD-JWT VC. + * + * @author Francis Pouatcha + */ +public class SdJwtSalt implements Comparable { + private final String salt; + + public SdJwtSalt(String salt) { + this.salt = SdJwtUtils.requireNonEmpty(salt, "salt must not be empty"); + } + + // Handy factory method + public static SdJwtSalt of(String salt) { + return new SdJwtSalt(salt); + } + + @Override + public String toString() { + return salt; + } + + @Override + public int compareTo(SdJwtSalt o) { + return salt.compareTo(o.salt); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/SdJwtUtils.java b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtUtils.java new file mode 100644 index 0000000..cddeeb1 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/SdJwtUtils.java @@ -0,0 +1,92 @@ + +package com.adorsys.ssi.sdjwt; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Optional; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.jose.jws.crypto.HashUtils; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.MinimalPrettyPrinter; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +/** + * + * @author Francis Pouatcha + */ +public class SdJwtUtils { + + public static final ObjectMapper mapper = new ObjectMapper(); + private static SecureRandom RANDOM = new SecureRandom(); + + public static String encodeNoPad(byte[] bytes) { + return Base64Url.encode(bytes); + } + + public static String hashAndBase64EncodeNoPad(byte[] disclosureBytes, String hashAlg) { + return encodeNoPad(HashUtils.hash(hashAlg, disclosureBytes)); + } + + public static String requireNonEmpty(String str, String message) { + return Optional.ofNullable(str) + .filter(s -> !s.isEmpty()) + .orElseThrow(() -> new IllegalArgumentException(message)); + } + + public static String randomSalt() { + // 16 bytes for 128-bit entropy. + // Base64url-encoded + return encodeNoPad(randomBytes(16)); + } + + public static byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + RANDOM.nextBytes(bytes); + return bytes; + } + + public static String printJsonArray(Object[] array) throws JsonProcessingException { + if (arrayEltSpaced) { + return arraySpacedPrettyPrinter.writer.writeValueAsString(array); + } else { + return mapper.writeValueAsString(array); + } + } + + static ArraySpacedPrettyPrinter arraySpacedPrettyPrinter = new ArraySpacedPrettyPrinter(); + + static class ArraySpacedPrettyPrinter extends MinimalPrettyPrinter { + final ObjectMapper prettyPrinObjectMapper; + final ObjectWriter writer; + + public ArraySpacedPrettyPrinter() { + prettyPrinObjectMapper = new ObjectMapper(); + prettyPrinObjectMapper.setDefaultPrettyPrinter(this); + writer = prettyPrinObjectMapper.writer(this); + } + + @Override + public void writeArrayValueSeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(','); + jg.writeRaw(' '); + } + + @Override + public void writeObjectEntrySeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(','); + jg.writeRaw(' '); // Add a space after comma + } + + @Override + public void writeObjectFieldValueSeparator(JsonGenerator jg) throws IOException { + jg.writeRaw(':'); + jg.writeRaw(' '); // Add a space after comma + } + } + + public static boolean arrayEltSpaced = true; +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedArrayElement.java b/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedArrayElement.java new file mode 100644 index 0000000..6400e94 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedArrayElement.java @@ -0,0 +1,54 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class UndisclosedArrayElement extends Disclosable implements SdJwtArrayElement { + private final JsonNode arrayElement; + + private UndisclosedArrayElement(SdJwtSalt salt, JsonNode arrayElement) { + super(salt); + this.arrayElement = arrayElement; + } + + @Override + public JsonNode getVisibleValue(String hashAlg) { + return SdJwtUtils.mapper.createObjectNode().put("...", getDisclosureDigest(hashAlg)); + } + + @Override + Object[] toArray() { + return new Object[] { getSaltAsString(), arrayElement }; + } + + public static class Builder { + private SdJwtSalt salt; + private JsonNode arrayElement; + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder withArrayElement(JsonNode arrayElement) { + this.arrayElement = arrayElement; + return this; + } + + public UndisclosedArrayElement build() { + arrayElement = Objects.requireNonNull(arrayElement, "arrayElement must not be null"); + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new UndisclosedArrayElement(salt, arrayElement); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedClaim.java b/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedClaim.java new file mode 100644 index 0000000..38a4646 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/UndisclosedClaim.java @@ -0,0 +1,83 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class UndisclosedClaim extends Disclosable implements SdJwtClaim { + private final SdJwtClaimName claimName; + private final JsonNode claimValue; + + private UndisclosedClaim(SdJwtClaimName claimName, SdJwtSalt salt, JsonNode claimValue) { + super(salt); + this.claimName = claimName; + this.claimValue = claimValue; + } + + @Override + Object[] toArray() { + return new Object[] { getSaltAsString(), getClaimNameAsString(), claimValue }; + } + + @Override + public SdJwtClaimName getClaimName() { + return claimName; + } + + @Override + public String getClaimNameAsString() { + return claimName.toString(); + } + + /** + * Recall no info is visible on these claims in the JWT. + */ + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + throw new UnsupportedOperationException("Unimplemented method 'getVisibleClaimValue'"); + } + + public static class Builder { + private SdJwtClaimName claimName; + private SdJwtSalt salt; + private JsonNode claimValue; + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withSalt(SdJwtSalt salt) { + this.salt = salt; + return this; + } + + public Builder withClaimValue(JsonNode claimValue) { + this.claimValue = claimValue; + return this; + } + + public UndisclosedClaim build() { + claimName = Objects.requireNonNull(claimName, "claimName must not be null"); + claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null"); + salt = salt == null ? new SdJwtSalt(SdJwtUtils.randomSalt()) : salt; + return new UndisclosedClaim(claimName, salt, claimValue); + } + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public List getDisclosureStrings() { + return Collections.singletonList(getDisclosureString()); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/VisibleArrayElement.java b/src/main/java/com/adorsys/ssi/sdjwt/VisibleArrayElement.java new file mode 100644 index 0000000..46b2406 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/VisibleArrayElement.java @@ -0,0 +1,26 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class VisibleArrayElement implements SdJwtArrayElement { + private final JsonNode arrayElement; + + public VisibleArrayElement(JsonNode arrayElement) { + this.arrayElement = arrayElement; + } + + @Override + public JsonNode getVisibleValue(String hashAlg) { + return arrayElement; + } + + @Override + public String getDisclosureString() { + return null; + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/VisibleSdJwtClaim.java b/src/main/java/com/adorsys/ssi/sdjwt/VisibleSdJwtClaim.java new file mode 100644 index 0000000..1f7df38 --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/VisibleSdJwtClaim.java @@ -0,0 +1,58 @@ + +package com.adorsys.ssi.sdjwt; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * + * @author Francis Pouatcha + */ +public class VisibleSdJwtClaim extends AbstractSdJwtClaim { + private final JsonNode claimValue; + + public VisibleSdJwtClaim(SdJwtClaimName claimName, JsonNode claimValue) { + super(claimName); + this.claimValue = claimValue; + } + + @Override + public JsonNode getVisibleClaimValue(String hashAlgo) { + return claimValue; + } + + // Static method to create a builder instance + public static Builder builder() { + return new Builder(); + } + + // Static inner Builder class + public static class Builder { + private SdJwtClaimName claimName; + private JsonNode claimValue; + + public Builder withClaimName(String claimName) { + this.claimName = new SdJwtClaimName(claimName); + return this; + } + + public Builder withClaimValue(JsonNode claimValue) { + this.claimValue = claimValue; + return this; + } + + public VisibleSdJwtClaim build() { + claimName = Objects.requireNonNull(claimName, "claimName must not be null"); + claimValue = Objects.requireNonNull(claimValue, "claimValue must not be null"); + return new VisibleSdJwtClaim(claimName, claimValue); + } + } + + @Override + public List getDisclosureStrings() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/vp/KeyBindingJWT.java b/src/main/java/com/adorsys/ssi/sdjwt/vp/KeyBindingJWT.java new file mode 100644 index 0000000..75c322d --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/vp/KeyBindingJWT.java @@ -0,0 +1,32 @@ + +package com.adorsys.ssi.sdjwt.vp; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.sdjwt.SdJws; + +/** + * + * @author Francis Pouatcha + * + */ +public class KeyBindingJWT extends SdJws { + + public static KeyBindingJWT of(String jwsString) { + return new KeyBindingJWT(jwsString); + } + + public static KeyBindingJWT from(JsonNode payload, SignatureSignerContext signer, String jwsType) { + JWSInput jwsInput = sign(payload, signer, jwsType); + return new KeyBindingJWT(payload, jwsInput); + } + + private KeyBindingJWT(JsonNode payload, JWSInput jwsInput) { + super(payload, jwsInput); + } + + private KeyBindingJWT(String jwsString) { + super(jwsString); + } +} diff --git a/src/main/java/com/adorsys/ssi/sdjwt/vp/SdJwtVP.java b/src/main/java/com/adorsys/ssi/sdjwt/vp/SdJwtVP.java new file mode 100644 index 0000000..0641a2d --- /dev/null +++ b/src/main/java/com/adorsys/ssi/sdjwt/vp/SdJwtVP.java @@ -0,0 +1,250 @@ + +package com.adorsys.ssi.sdjwt.vp; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +import org.keycloak.common.util.Base64Url; +import org.keycloak.crypto.SignatureSignerContext; +import com.adorsys.ssi.sdjwt.IssuerSignedJWT; +import com.adorsys.ssi.sdjwt.SdJwt; +import com.adorsys.ssi.sdjwt.SdJwtUtils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Francis Pouatcha + */ +public class SdJwtVP { + private String sdJwtVpString; + private final IssuerSignedJWT issuerSignedJWT; + + private final Map claims; + private final Map disclosures; + private final Map recursiveDigests; + private final List ghostDigests; + private final String hashAlgorithm; + + private final Optional keyBindingJWT; + + public IssuerSignedJWT getIssuerSignedJWT() { + return issuerSignedJWT; + } + + public Map getDisclosures() { + return disclosures; + } + + public Collection getDisclosuresString() { + return disclosures.values(); + } + + public Map getRecursiveDigests() { + return recursiveDigests; + } + + public Collection getGhostDigests() { + return ghostDigests; + } + + public String getHashAlgorithm() { + return hashAlgorithm; + } + + public Optional getKeyBindingJWT() { + return keyBindingJWT; + } + + private SdJwtVP(String sdJwtVpString, String hashAlgorithm, IssuerSignedJWT issuerSignedJWT, + Map claims, Map disclosures, Map recursiveDigests, + List ghostDigests, Optional keyBindingJWT) { + this.sdJwtVpString = sdJwtVpString; + this.hashAlgorithm = hashAlgorithm; + this.issuerSignedJWT = issuerSignedJWT; + this.claims = Collections.unmodifiableMap(claims); + this.disclosures = Collections.unmodifiableMap(disclosures); + this.recursiveDigests = Collections.unmodifiableMap(recursiveDigests); + this.ghostDigests = Collections.unmodifiableList(ghostDigests); + this.keyBindingJWT = keyBindingJWT; + } + + public static SdJwtVP of(String sdJwtString) { + int disclosureStart = sdJwtString.indexOf(SdJwt.DELIMITER); + int disclosureEnd = sdJwtString.lastIndexOf(SdJwt.DELIMITER); + + String issuerSignedJWTString = sdJwtString.substring(0, disclosureStart); + String disclosuresString = sdJwtString.substring(disclosureStart + 1, disclosureEnd); + + IssuerSignedJWT issuerSignedJWT = IssuerSignedJWT.fromJws(issuerSignedJWTString); + + ObjectNode issuerPayload = (ObjectNode) issuerSignedJWT.getPayload(); + String hashAlgorithm = issuerPayload.get(IssuerSignedJWT.CLAIM_NAME_SD_HASH_ALGORITHM).asText(); + + Map claims = new HashMap<>(); + Map disclosures = new HashMap<>(); + + String[] split = disclosuresString.split(SdJwt.DELIMITER); + for (String disclosure : split) { + String disclosureDigest = SdJwtUtils.hashAndBase64EncodeNoPad(disclosure.getBytes(), hashAlgorithm); + if (disclosures.containsKey(disclosureDigest)) { + throw new IllegalArgumentException("Duplicate disclosure digest"); + } + disclosures.put(disclosureDigest, disclosure); + ArrayNode disclosureData; + try { + disclosureData = (ArrayNode) SdJwtUtils.mapper.readTree(Base64Url.decode(disclosure)); + claims.put(disclosureDigest, disclosureData); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid disclosure data"); + } + } + Set allDigests = claims.keySet(); + + Map recursiveDigests = new HashMap<>(); + List ghostDigests = new ArrayList<>(); + allDigests.stream() + .forEach(disclosureDigest -> { + JsonNode node = findNode(issuerPayload, disclosureDigest); + node = processDisclosureDigest(node, disclosureDigest, claims, recursiveDigests, ghostDigests); + }); + + Optional keyBindingJWT = Optional.empty(); + if (sdJwtString.length() > disclosureEnd + 1) { + String keyBindingJWTString = sdJwtString.substring(disclosureEnd + 1); + keyBindingJWT = Optional.of(KeyBindingJWT.of(keyBindingJWTString)); + } + + // Drop the key binding String if any. As it is held by the keyBindingJwtObject + String sdJWtVPString = sdJwtString.substring(0, disclosureEnd + 1); + + return new SdJwtVP(sdJWtVPString, hashAlgorithm, issuerSignedJWT, claims, disclosures, recursiveDigests, + ghostDigests, keyBindingJWT); + + } + + private static JsonNode processDisclosureDigest(JsonNode node, String disclosureDigest, + Map claims, + Map recursiveDigests, + List ghostDigests) { + if (node == null) { // digest is nested in another disclosure + Set> entrySet = claims.entrySet(); + for (Entry entry : entrySet) { + if (entry.getKey().equals(disclosureDigest)) { + continue; + } + node = findNode(entry.getValue(), disclosureDigest); + if (node != null) { + recursiveDigests.put(disclosureDigest, entry.getKey()); + break; + } + } + } + if (node == null) { // No digest found for disclosure. + ghostDigests.add(disclosureDigest); + } + return node; + } + + public JsonNode getCnfClaim() { + return issuerSignedJWT.getPayload().get("cnf"); + } + + public String present(List disclosureDigests, JsonNode keyBindingClaims, + SignatureSignerContext holdSignatureSignerContext, String jwsType) { + StringBuilder sb = new StringBuilder(); + if (disclosureDigests == null || disclosureDigests.isEmpty()) { + // disclose everything + sb.append(sdJwtVpString); + } else { + sb.append(issuerSignedJWT.toJws()); + sb.append(SdJwt.DELIMITER); + for (String disclosureDigest : disclosureDigests) { + sb.append(disclosures.get(disclosureDigest)); + sb.append(SdJwt.DELIMITER); + } + } + String unboundPresentation = sb.toString(); + if (keyBindingClaims == null || holdSignatureSignerContext == null) { + return unboundPresentation; + } + String sd_hash = SdJwtUtils.hashAndBase64EncodeNoPad(unboundPresentation.getBytes(), getHashAlgorithm()); + keyBindingClaims = ((ObjectNode) keyBindingClaims).put("sd_hash", sd_hash); + KeyBindingJWT keyBindingJWT = KeyBindingJWT.from(keyBindingClaims, holdSignatureSignerContext, jwsType); + sb.append(keyBindingJWT.getJwsString()); + return sb.toString(); + } + + // Recursively seraches the node with the given value. + // Returns the node if found, null otherwise. + private static JsonNode findNode(JsonNode node, String value) { + if (node == null) { + return null; + } + if (node.isValueNode()) { + if (node.asText().equals(value)) { + return node; + } else { + return null; + } + } + if (node.isArray() || node.isObject()) { + for (JsonNode child : node) { + JsonNode found = findNode(child, value); + if (found != null) { + return found; + } + } + } + return null; + } + + @Override + public String toString() { + return sdJwtVpString; + } + + public String verbose() { + StringBuilder sb = new StringBuilder(); + sb.append("Issuer Signed JWT: "); + sb.append(issuerSignedJWT.getPayload()); + sb.append("\n"); + disclosures.forEach((digest, disclosure) -> { + sb.append("\n"); + sb.append("Digest: "); + sb.append(digest); + sb.append("\n"); + sb.append("Disclosure: "); + sb.append(disclosure); + sb.append("\n"); + sb.append("Content: "); + sb.append(claims.get(digest)); + sb.append("\n"); + }); + sb.append("\n"); + sb.append("Recursive Digests: "); + sb.append(recursiveDigests); + sb.append("\n"); + sb.append("\n"); + sb.append("Ghost Digests: "); + sb.append(ghostDigests); + sb.append("\n"); + sb.append("\n"); + if (keyBindingJWT.isPresent()) { + sb.append("Key Binding JWT: "); + sb.append("\n"); + sb.append(keyBindingJWT.get().getPayload().toString()); + sb.append("\n"); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementDisclosureTest.java b/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementDisclosureTest.java new file mode 100644 index 0000000..0ad8d0e --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementDisclosureTest.java @@ -0,0 +1,65 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwt; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class ArrayElementDisclosureTest { + + @Test + public void testSdJwtWithUndiclosedArrayElements6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw") + .withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA") + .build(); + + org.keycloak.sdjwt.SdJwt sdJwt = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .build(); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), + "sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json"); + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testSdJwtWithUndiclosedAndDecoyArrayElements6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw") + .withUndisclosedArrayElt("nationalities", 0, "Qg_O64zqAxe412a108iroA") + .withUndisclosedArrayElt("nationalities", 1, "nPuoQnkRFq3BIeAm7AnXFA") + .withDecoyArrayElt("nationalities", 1, "5bPs1IquZNa0hkaFzzzZNw") + .build(); + + org.keycloak.sdjwt.SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(claimSet) + .build(); + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), + "sdjwt/s6.1-issuer-payload-decoy-array-ellement.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementSerializationTest.java b/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementSerializationTest.java new file mode 100644 index 0000000..85248ca --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/ArrayElementSerializationTest.java @@ -0,0 +1,44 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.sdjwt.SdJwtSalt; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.UndisclosedArrayElement; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class ArrayElementSerializationTest { + + @Before + public void setUp() throws Exception { + org.keycloak.sdjwt.SdJwtUtils.arrayEltSpaced = false; + } + + @After + public void tearDown() throws Exception { + SdJwtUtils.arrayEltSpaced = true; + } + + @Test + public void testToBase64urlEncoded() { + // Create an instance of UndisclosedArrayElement with the specified fields + // "lklxF5jMYlGTPUovMNIvCA", "FR" + org.keycloak.sdjwt.UndisclosedArrayElement arrayElementDisclosure = UndisclosedArrayElement.builder() + .withSalt(new SdJwtSalt("lklxF5jMYlGTPUovMNIvCA")) + .withArrayElement(new TextNode("FR")).build(); + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, arrayElementDisclosure.getDisclosureString()); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/DisclosureRedListTest.java b/src/test/java/com/adorsys/ssi/sdjwt/DisclosureRedListTest.java new file mode 100644 index 0000000..244dbf5 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/DisclosureRedListTest.java @@ -0,0 +1,52 @@ + +package com.adorsys.ssi.sdjwt; + +import org.junit.Test; +import org.keycloak.sdjwt.DisclosureSpec; + +public class DisclosureRedListTest { + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInObjectClaim() { + org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("vct") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInArrayClaim() { + org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedArrayElt("iat", 0, "2GLC42sKQveCfGfryNRN9w") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInDecoyArrayClaim() { + org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withDecoyArrayElt("exp", 0, "2GLC42sKQveCfGfryNRN9w") + .build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedIss() { + org.keycloak.sdjwt.DisclosureSpec.builder().withUndisclosedClaim("iss").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedInObjectNbf() { + org.keycloak.sdjwt.DisclosureSpec.builder().withUndisclosedClaim("nbf").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedCnf() { + org.keycloak.sdjwt.DisclosureSpec.builder().withUndisclosedClaim("cnf").build(); + } + + @Test(expected = IllegalArgumentException.class) + public void testDefaultRedListedStatus() { + DisclosureSpec.builder().withUndisclosedClaim("status").build(); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/IssuerSignedJWTTest.java b/src/test/java/com/adorsys/ssi/sdjwt/IssuerSignedJWTTest.java new file mode 100644 index 0000000..c4b2ebe --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/IssuerSignedJWTTest.java @@ -0,0 +1,120 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwt; +import org.keycloak.sdjwt.SdJwtClaim; +import org.keycloak.sdjwt.VisibleSdJwtClaim; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Francis Pouatcha + */ +public class IssuerSignedJWTTest { + /** + * If issuer decides to disclose everything, paylod of issuer signed JWT should + * be same as the claim set. + * + * This is essential for backward compatibility with non sd based jwt issuance. + * + * @throws IOException + */ + @Test + public void testIssuerSignedJWTPayloadWithValidClaims() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + List claims = new ArrayList<>(); + claimSet.fields().forEachRemaining(entry -> { + claims.add( + org.keycloak.sdjwt.VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = org.keycloak.sdjwt.IssuerSignedJWT.builder().withClaims(claims).build(); + + assertEquals(claimSet, jwt.getPayload()); + } + + @Test + public void testIssuerSignedJWTPayloadThrowsExceptionForDuplicateClaims() throws IOException { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + List claims = new ArrayList<>(); + + // First fill claims + claimSet.fields().forEachRemaining(entry -> { + claims.add( + org.keycloak.sdjwt.VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + // First fill claims + claimSet.fields().forEachRemaining(entry -> { + claims.add( + VisibleSdJwtClaim.builder().withClaimName(entry.getKey()).withClaimValue(entry.getValue()).build()); + }); + + // All claims are duplicate. + assertTrue(claims.size() == claimSet.size() * 2); + + // Expecting exception + assertThrows(IllegalArgumentException.class, () -> org.keycloak.sdjwt.IssuerSignedJWT.builder().withClaims(claims).build()); + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims6_1() { + JsonNode claimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-holder-claims.json"); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("email", "JnwGqRFZjMprsoZobherdQ") + .withUndisclosedClaim("phone_number", "ffZ03jm_zeHyG4-yoNt6vg") + .withUndisclosedClaim("address", "INhOGJnu82BAtsOwiCJc_A") + .withUndisclosedClaim("birthdate", "d0l3jsh5sBzj2oEhZxrJGw").build(); + + org.keycloak.sdjwt.SdJwt sdJwt = org.keycloak.sdjwt.SdJwt.builder().withDisclosureSpec(disclosureSpec).withClaimSet(claimSet).build(); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s6.1-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims3_3() { + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json"); + + // Merge both + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + org.keycloak.sdjwt.SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .build(); + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/JsonClaimsetTest.java b/src/test/java/com/adorsys/ssi/sdjwt/JsonClaimsetTest.java new file mode 100644 index 0000000..7830abd --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/JsonClaimsetTest.java @@ -0,0 +1,63 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import org.junit.Test; +import org.keycloak.sdjwt.SdJwtUtils; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class JsonClaimsetTest { + + @Test + public void testRead61ClaimSet() throws IOException { + InputStream is = getClass().getClassLoader().getResourceAsStream("sdjwt/s6.1-holder-claims.json"); + JsonNode claimSet = SdJwtUtils.mapper.readTree(is); + + // Test reading a String + String expected_sub_claim = "user_42"; + JsonNode sub_claim = claimSet.get("sub"); + assertEquals(JsonNodeType.STRING, sub_claim.getNodeType()); + assertEquals(expected_sub_claim, sub_claim.asText()); + + // Test reading a boolean + JsonNode phone_number_verified_claim = claimSet.get("phone_number_verified"); + Boolean expected_phone_number_verified_claim = true; + assertEquals(JsonNodeType.BOOLEAN, phone_number_verified_claim.getNodeType()); + assertEquals(expected_phone_number_verified_claim, phone_number_verified_claim.asBoolean()); + + // Test reading an object + JsonNode address_claim = claimSet.get("address"); + assertEquals(JsonNodeType.OBJECT, address_claim.getNodeType()); + JsonNode street_address_claim = address_claim.get("street_address"); + assertEquals(JsonNodeType.STRING, street_address_claim.getNodeType()); + String expected_street_address_claim = "123 Main St"; + assertEquals(expected_street_address_claim, street_address_claim.asText()); + + // Test reading a number + JsonNode updated_at_claim = claimSet.get("updated_at"); + int expected_updated_at_claim = 1570000000; + assertEquals(JsonNodeType.NUMBER, updated_at_claim.getNodeType()); + assertEquals(expected_updated_at_claim, updated_at_claim.asInt()); + + // Test reading an array + JsonNode nationalities_claim = claimSet.get("nationalities"); + assertEquals(JsonNodeType.ARRAY, nationalities_claim.getNodeType()); + assertEquals(2, nationalities_claim.size()); + JsonNode element_0_nationalities_claim = nationalities_claim.get(0); + assertEquals(JsonNodeType.STRING, element_0_nationalities_claim.getNodeType()); + String expected_element_0_nationalities_claim = "US"; + assertEquals(expected_element_0_nationalities_claim, element_0_nationalities_claim.asText()); + JsonNode element_1_nationalities_claim = nationalities_claim.get(1); + assertEquals(JsonNodeType.STRING, element_1_nationalities_claim.getNodeType()); + String expected_element_1_nationalities_claim = "DE"; + assertEquals(expected_element_1_nationalities_claim, element_1_nationalities_claim.asText()); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/JsonNodeComparisonTest.java b/src/test/java/com/adorsys/ssi/sdjwt/JsonNodeComparisonTest.java new file mode 100644 index 0000000..8cc1595 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/JsonNodeComparisonTest.java @@ -0,0 +1,23 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class JsonNodeComparisonTest { + @Test + public void testJsonNodeEquality() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + JsonNode node1 = mapper.readTree("{\"name\":\"John\", \"age\":30}"); + JsonNode node2 = mapper.readTree("{\"age\":30, \"name\":\"John\"}"); + + assertEquals("JsonNode objects should be equal", node1, node2); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/SdJWTSamplesTest.java b/src/test/java/com/adorsys/ssi/sdjwt/SdJWTSamplesTest.java new file mode 100644 index 0000000..9ba8d57 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/SdJWTSamplesTest.java @@ -0,0 +1,162 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwt; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class SdJWTSamplesTest { + @Test + public void testS7_1_FlatSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + // produce the main sdJwt, adding nested sdJwts + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("address", "2GLC42sKQveCfGfryNRN9w") + .build(); + org.keycloak.sdjwt.SdJwt sdJwt = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .build(); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.1-issuer-payload.json"); + + assertEquals(expected, jwt.getPayload()); + } + + @Test + public void testS7_2_StructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + org.keycloak.sdjwt.DisclosureSpec addrDisclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + org.keycloak.sdjwt.SdJwt addrSdJWT = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder().build(); + // produce the main sdJwt, adding nested sdJwts + org.keycloak.sdjwt.SdJwt sdJwt = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + } + + @Test + public void testS7_2b_PartialDisclosureOfStructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + org.keycloak.sdjwt.DisclosureSpec addrDisclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + org.keycloak.sdjwt.SdJwt addrSdJWT = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder().build(); + // produce the main sdJwt, adding nested sdJwts + org.keycloak.sdjwt.SdJwt sdJwt = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + org.keycloak.sdjwt.IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.2b-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + } + + @Test + public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() { + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s7-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + org.keycloak.sdjwt.DisclosureSpec addrDisclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("locality", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("region", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("country", "eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + // produce the nested sdJwt + org.keycloak.sdjwt.SdJwt addrSdJWT = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + // cleanup e.g nested _sd_alg + JsonNode addPayload = addrSdJWT.asNestedPayload(); + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .build(); + // produce the main sdJwt, adding nested sdJwts + org.keycloak.sdjwt.SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s7.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/SdJwtTest.java b/src/test/java/com/adorsys/ssi/sdjwt/SdJwtTest.java new file mode 100644 index 0000000..cb8feac --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/SdJwtTest.java @@ -0,0 +1,92 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.keycloak.crypto.SignatureSignerContext; +import org.keycloak.sdjwt.DisclosureSpec; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Francis Pouatcha + */ +public class SdJwtTest { + + @Test + public void settingsTest() { + SignatureSignerContext issuerSignerContext = TestSettings.getInstance().getIssuerSignerContext(); + assertNotNull(issuerSignerContext); + } + + @Test + public void testA1_Example2_with_nested_disclosure_and_decoy_claims() { + org.keycloak.sdjwt.DisclosureSpec addrDisclosureSpec = org.keycloak.sdjwt.DisclosureSpec.builder() + .withUndisclosedClaim("street_address", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("locality", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("region", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("country", "lklxF5jMYlGTPUovMNIvCA") + .withDecoyClaim("2GLC42sKQveCfGfryNRN9w") + .withDecoyClaim("eluV5Og3gSNII8EYnsxA_A") + .withDecoyClaim("6Ij7tM-a5iVPGboS5tmvVA") + .withDecoyClaim("eI8ZWm9QnKPpNPeNenHdhQ") + .build(); + + org.keycloak.sdjwt.DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("sub", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("given_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("family_name", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("email", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("phone_number", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "yytVbdAPGcgl2rI4C9GSog") + .withDecoyClaim("AJx-095VPrpTtN4QMOqROA") + .withDecoyClaim("G02NSrQfjFXQ7Io09syajA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-holder-claims.json"); + + // Read claims provided by the holder + JsonNode addressClaimSet = holderClaimSet.get("address"); + + // produce the nested sdJwt + org.keycloak.sdjwt.SdJwt addrSdJWT = org.keycloak.sdjwt.SdJwt.builder() + .withDisclosureSpec(addrDisclosureSpec) + .withClaimSet(addressClaimSet) + .build(); + JsonNode addPayload = addrSdJWT.asNestedPayload(); + JsonNode expectedAddrPayload = TestUtils.readClaimSet(getClass(), + "sdjwt/a1.example2-address-payload.json"); + assertEquals(expectedAddrPayload, addPayload); + + // Verify nested claim has 4 disclosures + assertEquals(4, addrSdJWT.getDisclosures().size()); + + // Set payload back into main claim set + ((ObjectNode) holderClaimSet).set("address", addPayload); + + // Read claims added by the issuer & merge both + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-claims.json"); + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + // produce the main sdJwt, adding nested sdJwts + org.keycloak.sdjwt.SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withNestedSdJwt(addrSdJWT) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/a1.example2-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + // Verify all claims are present. + // 10 disclosures from 16 digests (6 decoy claims & decoy array elements) + assertEquals(10, sdJwt.getDisclosures().size()); + } + +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/SdJwtUtilsTest.java b/src/test/java/com/adorsys/ssi/sdjwt/SdJwtUtilsTest.java new file mode 100644 index 0000000..4191550 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/SdJwtUtilsTest.java @@ -0,0 +1,110 @@ + +package com.adorsys.ssi.sdjwt; + +import org.junit.Test; +import org.keycloak.jose.jws.crypto.HashUtils; +import org.keycloak.sdjwt.SdJwtUtils; + +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class SdJwtUtilsTest { + /** + * Verify hash production and base 64 url encoding + * Verify algorithm denomination for keycloak encoding. + */ + @Test + public void testHashDisclosure() { + String expected = "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY"; + byte[] hash = HashUtils.hash("SHA-256", "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0".getBytes()); + assertEquals(expected, org.keycloak.sdjwt.SdJwtUtils.encodeNoPad(hash)); + } + + /** + * Verify hash production and base 64 url encoding + * Verify algorithm denomination for keycloak encoding. + */ + @Test + public void testHashDisclosure2() { + String expected = "w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs"; + byte[] hash = HashUtils.hash("SHA-256", "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0".getBytes()); + assertEquals(expected, org.keycloak.sdjwt.SdJwtUtils.encodeNoPad(hash)); + } + + /** + * Test the base64 URL encoding of this json string from the spec, + * with whitespace between array elements. + * + * ["_26bc4LT-ac6q2KI6cBW5es", "family_name", "Möbius"] + * + * shall produce + * WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0 + * + * There is no padding in the expected string. + * + * see + * https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1 + * + * @throws IOException + */ + @Test + public void testBase64urlEncodedObjectWhiteSpacedJsonArray() { + String input = "[\"_26bc4LT-ac6q2KI6cBW5es\", \"family_name\", \"Möbius\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, org.keycloak.sdjwt.SdJwtUtils.encodeNoPad(input.getBytes())); + } + + /** + * As we are expexting json serializer to behave differently + * + * https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html#section-5.2.1 + * + * @throws IOException + */ + @Test + public void testBase64urlEncodedObjectNoWhiteSpacedJsonArray() { + // Test the base64 URL encoding of this json string from the spec, + // no whitespace between array elements + String input = "[\"_26bc4LT-ac6q2KI6cBW5es\",\"family_name\",\"Möbius\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, org.keycloak.sdjwt.SdJwtUtils.encodeNoPad(input.getBytes())); + } + + @Test + public void testBase64urlEncodedArrayElementWhiteSpacedJsonArray() { + String input = "[\"lklxF5jMYlGTPUovMNIvCA\", \"FR\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, org.keycloak.sdjwt.SdJwtUtils.encodeNoPad(input.getBytes())); + } + + @Test + public void testBase64urlEncodedArrayElementNoWhiteSpacedJsonArray() { + String input = "[\"lklxF5jMYlGTPUovMNIvCA\",\"FR\"]"; + + // Expected Base64 URL encoded string + String expected = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwiRlIiXQ"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, SdJwtUtils.encodeNoPad(input.getBytes())); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/TestSettings.java b/src/test/java/com/adorsys/ssi/sdjwt/TestSettings.java new file mode 100644 index 0000000..7908684 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/TestSettings.java @@ -0,0 +1,205 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.util.Base64Url; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.crypto.*; + +import java.math.BigInteger; +import java.security.*; +import java.security.spec.*; +import java.util.HashMap; +import java.util.Map; + +/** + * Import test-settings from: + * https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml + * + * @author Francis Pouatcha + */ +public class TestSettings { + public final SignatureSignerContext holderSigContext; + public final SignatureSignerContext issuerSigContext; + public final SignatureVerifierContext holderVerifierContext; + public final SignatureVerifierContext issuerVerifierContext; + + private static TestSettings instance = null; + + public static TestSettings getInstance() { + if (instance == null) { + instance = new TestSettings(); + } + return instance; + } + + public SignatureSignerContext getIssuerSignerContext() { + return issuerSigContext; + } + + public SignatureSignerContext getHolderSignerContext() { + return holderSigContext; + } + + public SignatureVerifierContext getIssuerVerifierContext() { + return issuerVerifierContext; + } + + public SignatureVerifierContext getHolderVerifierContext() { + return holderVerifierContext; + } + + // private constructor + private TestSettings() { + JsonNode testSettings = TestUtils.readClaimSet(getClass(), "sdjwt/test-settings.json"); + JsonNode keySettings = testSettings.get("key_settings"); + + holderSigContext = initSigContext(keySettings, "holder_key", "ES256", "holder"); + issuerSigContext = initSigContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022"); + + holderVerifierContext = initVerifierContext(keySettings, "holder_key", "ES256", "holder"); + issuerVerifierContext = initVerifierContext(keySettings, "issuer_key", "ES256", "doc-signer-05-25-2022"); + } + + private static SignatureSignerContext initSigContext(JsonNode keySettings, String keyName, String algorithm, + String kid) { + JsonNode keySetting = keySettings.get(keyName); + KeyPair keyPair = readKeyPair(keySetting); + return getSignatureSignerContext(keyPair, algorithm, kid); + } + + private static SignatureVerifierContext initVerifierContext(JsonNode keySettings, String keyName, String algorithm, + String kid) { + JsonNode keySetting = keySettings.get(keyName); + KeyPair keyPair = readKeyPair(keySetting); + return getSignatureVerifierContext(keyPair.getPublic(), algorithm, kid); + } + + private static KeyPair readKeyPair(JsonNode keySetting) { + String curveName = keySetting.get("crv").asText(); + String base64UrlEncodedD = keySetting.get("d").asText(); + String base64UrlEncodedX = keySetting.get("x").asText(); + String base64UrlEncodedY = keySetting.get("y").asText(); + return readEcdsaKeyPair(curveName, base64UrlEncodedD, base64UrlEncodedX, base64UrlEncodedY); + } + + public static SignatureVerifierContext verifierContextFrom(JsonNode keyData, String algorithm) { + PublicKey publicKey = readPublicKey(keyData); + return getSignatureVerifierContext(publicKey, algorithm, KeyUtils.createKeyId(publicKey)); + } + + private static PublicKey readPublicKey(JsonNode keyData) { + if (keyData.has("jwk")) { + keyData = keyData.get("jwk"); + } + String curveName = keyData.get("crv").asText(); + String base64UrlEncodedX = keyData.get("x").asText(); + String base64UrlEncodedY = keyData.get("y").asText(); + return readEcdsaPublic(curveName, base64UrlEncodedX, base64UrlEncodedY); + } + + private static PublicKey readEcdsaPublic(String curveName, String base64UrlEncodedX, + String base64UrlEncodedY) { + + ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName)); + + byte[] xBytes = Base64Url.decode(base64UrlEncodedX); + byte[] yBytes = Base64Url.decode(base64UrlEncodedY); + + try { + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + + // Generate ECPrivateKey + + // Instantiate ECPoint + BigInteger xValue = new BigInteger(1, xBytes); + BigInteger yValue = new BigInteger(1, yBytes); + ECPoint point = new ECPoint(xValue, yValue); + + // Generate ECPublicKey + return keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static KeyPair readEcdsaKeyPair(String curveName, String base64UrlEncodedD, String base64UrlEncodedX, + String base64UrlEncodedY) { + + ECParameterSpec ecSpec = getECParameterSpec(ECDSA_CURVE_2_SPECS_NAMES.get(curveName)); + + byte[] dBytes = Base64Url.decode(base64UrlEncodedD); + byte[] xBytes = Base64Url.decode(base64UrlEncodedX); + byte[] yBytes = Base64Url.decode(base64UrlEncodedY); + + try { + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + + // Generate ECPrivateKey + BigInteger dValue = new BigInteger(1, dBytes); + PrivateKey privateKey = keyFactory.generatePrivate(new ECPrivateKeySpec(dValue, ecSpec)); + + // Instantiate ECPoint + BigInteger xValue = new BigInteger(1, xBytes); + BigInteger yValue = new BigInteger(1, yBytes); + ECPoint point = new ECPoint(xValue, yValue); + + // Generate ECPublicKey + PublicKey publicKey = keyFactory.generatePublic(new ECPublicKeySpec(point, ecSpec)); + return new KeyPair(publicKey, privateKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static final Map ECDSA_KEY_SPECS = new HashMap<>(); + + private static ECParameterSpec getECParameterSpec(String paramSpecName) { + return ECDSA_KEY_SPECS.computeIfAbsent(paramSpecName, TestSettings::generateEcdsaKeySpec); + } + + // generate key spec + private static ECParameterSpec generateEcdsaKeySpec(String paramSpecName) { + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(paramSpecName); + keyPairGenerator.initialize(ecGenParameterSpec); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + return ((java.security.interfaces.ECPublicKey) keyPair.getPublic()).getParams(); + } catch (Exception e) { + throw new RuntimeException("Error obtaining ECParameterSpec for P-256 curve", e); + } + } + + private static SignatureSignerContext getSignatureSignerContext(KeyPair keyPair, String algorithm, String kid) { + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setPrivateKey(keyPair.getPrivate()); + keyWrapper.setPublicKey(keyPair.getPublic()); + keyWrapper.setType(keyPair.getPublic().getAlgorithm()); + keyWrapper.setUse(KeyUse.SIG); + keyWrapper.setKid(kid); + return new AsymmetricSignatureSignerContext(keyWrapper); + } + + private static SignatureVerifierContext getSignatureVerifierContext(PublicKey publicKey, String algorithm, + String kid) { + KeyWrapper keyWrapper = new KeyWrapper(); + keyWrapper.setAlgorithm(algorithm); + keyWrapper.setPublicKey(publicKey); + keyWrapper.setType(publicKey.getAlgorithm()); + keyWrapper.setUse(KeyUse.SIG); + keyWrapper.setKid(kid); + return new AsymmetricSignatureVerifierContext(keyWrapper); + } + + private static final Map ECDSA_CURVE_2_SPECS_NAMES = new HashMap<>(); + + private static final void curveToSpecName() { + ECDSA_CURVE_2_SPECS_NAMES.put("P-256", "secp256r1"); + } + + static { + curveToSpecName(); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/TestUtils.java b/src/test/java/com/adorsys/ssi/sdjwt/TestUtils.java new file mode 100644 index 0000000..8c6460f --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/TestUtils.java @@ -0,0 +1,48 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.sdjwt.SdJwtUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * @author Francis Pouatcha + */ +public class TestUtils { + public static JsonNode readClaimSet(Class klass, String path) { + // try-with-resources closes inputstream! + try (InputStream is = klass.getClassLoader().getResourceAsStream(path)) { + return SdJwtUtils.mapper.readTree(is); + } catch (IOException e) { + throw new RuntimeException("Error reading file at path: " + path, e); + } + } + + public static String readFileAsString(Class klass, String filePath) { + StringBuilder stringBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + (new InputStreamReader(klass.getClassLoader().getResourceAsStream(filePath))))) { + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line); // Appends line without a newline character + } + } catch (IOException e) { + throw new RuntimeException("Error reading file at path: " + filePath, e); + } + return stringBuilder.toString(); + } + + public static String splitStringIntoLines(String input, int lineLength) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < input.length(); i += lineLength) { + int end = Math.min(input.length(), i + lineLength); + result.append(input, i, end).append("\n"); + } + return result.toString(); + } + +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/UndisclosedClaimTest.java b/src/test/java/com/adorsys/ssi/sdjwt/UndisclosedClaimTest.java new file mode 100644 index 0000000..ea45ab3 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/UndisclosedClaimTest.java @@ -0,0 +1,45 @@ + +package com.adorsys.ssi.sdjwt; + +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.sdjwt.SdJwtSalt; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.UndisclosedClaim; + +import static org.junit.Assert.assertEquals; + +/** + * @author Francis Pouatcha + */ +public class UndisclosedClaimTest { + + @Before + public void setUp() throws Exception { + org.keycloak.sdjwt.SdJwtUtils.arrayEltSpaced = false; + } + + @After + public void tearDown() throws Exception { + SdJwtUtils.arrayEltSpaced = true; + } + + @Test + public void testToBase64urlEncoded() { + // Create an instance of UndisclosedClaim with the specified fields + org.keycloak.sdjwt.UndisclosedClaim undisclosedClaim = UndisclosedClaim.builder() + .withClaimName("family_name") + .withSalt(new SdJwtSalt("_26bc4LT-ac6q2KI6cBW5es")) + .withClaimValue(new TextNode("Möbius")) + .build(); + + // Expected Base64 URL encoded string + String expected = "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd"; + + // Assert that the base64 URL encoded string from the object matches the + // expected string + assertEquals(expected, undisclosedClaim.getDisclosureStrings().get(0)); + } +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/SdJwtVPTest.java b/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/SdJwtVPTest.java new file mode 100644 index 0000000..37a6fe8 --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/SdJwtVPTest.java @@ -0,0 +1,178 @@ + +package com.adorsys.ssi.sdjwt.sdjwtvp; + +import com.adorsys.ssi.sdjwt.TestSettings; +import com.adorsys.ssi.sdjwt.TestUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.keycloak.common.VerificationException; +import org.keycloak.sdjwt.*; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Francis Pouatcha + */ +public class SdJwtVPTest { + // Additional tests can be written to cover edge cases, error conditions, + // and any other functionality specific to the SdJwt class. + @Test + public void testIssuerSignedJWTWithUndiclosedClaims3_3() { + DisclosureSpec disclosureSpec = DisclosureSpec.builder() + .withUndisclosedClaim("given_name", "2GLC42sKQveCfGfryNRN9w") + .withUndisclosedClaim("family_name", "eluV5Og3gSNII8EYnsxA_A") + .withUndisclosedClaim("email", "6Ij7tM-a5iVPGboS5tmvVA") + .withUndisclosedClaim("phone_number", "eI8ZWm9QnKPpNPeNenHdhQ") + .withUndisclosedClaim("address", "Qg_O64zqAxe412a108iroA") + .withUndisclosedClaim("birthdate", "AJx-095VPrpTtN4QMOqROA") + .withUndisclosedClaim("is_over_18", "Pc33JM2LchcU_lHggv_ufQ") + .withUndisclosedClaim("is_over_21", "G02NSrQfjFXQ7Io09syajA") + .withUndisclosedClaim("is_over_65", "lklxF5jMYlGTPUovMNIvCA") + .build(); + + // Read claims provided by the holder + JsonNode holderClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-holder-claims.json"); + // Read claims added by the issuer + JsonNode issuerClaimSet = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-claims.json"); + + // Merge both + ((ObjectNode) holderClaimSet).setAll((ObjectNode) issuerClaimSet); + + SdJwt sdJwt = SdJwt.builder() + .withDisclosureSpec(disclosureSpec) + .withClaimSet(holderClaimSet) + .withSigner(TestSettings.getInstance().getIssuerSignerContext()) + .build(); + + IssuerSignedJWT jwt = sdJwt.getIssuerSignedJWT(); + + JsonNode expected = TestUtils.readClaimSet(getClass(), "sdjwt/s3.3-issuer-payload.json"); + assertEquals(expected, jwt.getPayload()); + + String sdJwtString = sdJwt.toSdJwtString(); + + SdJwtVP actualSdJwt = SdJwtVP.of(sdJwtString); + + String expectedString = TestUtils.readFileAsString(getClass(), "sdjwt/s3.3-unsecured-sd-jwt.txt"); + SdJwtVP expecteSdJwt = SdJwtVP.of(expectedString); + + TestCompareSdJwt.compare(expecteSdJwt, actualSdJwt); + + } + + @Test + public void testIssuerSignedJWTWithUndiclosedClaims6_1() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.1-issued-payload.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(0, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testA1_Example2_with_nested_disclosure_and_decoy_claims() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/a1.example2-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(10, sdJwtVP.getDisclosures().size()); + assertEquals(0, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_RecursiveDisclosureOfStructuredSdJwt() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(5, sdJwtVP.getDisclosures().size()); + assertEquals(4, sdJwtVP.getRecursiveDigests().size()); + assertEquals(0, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_GhostDisclosures() { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt+ghost.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + // System.out.println(sdJwtVP.verbose()); + assertEquals(8, sdJwtVP.getDisclosures().size()); + assertEquals(4, sdJwtVP.getRecursiveDigests().size()); + assertEquals(3, sdJwtVP.getGhostDigests().size()); + } + + @Test + public void testS7_3_VerifyIssuerSignaturePositive() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getIssuerVerifierContext()); + } + + @Test(expected = VerificationException.class) + public void testS7_3_VerifyIssuerSignatureNegative() throws VerificationException { + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s7.3-sdjwt.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + sdJwtVP.getIssuerSignedJWT().verifySignature(TestSettings.getInstance().getHolderVerifierContext()); + } + + @Test + public void testS6_2_PresentationPositive() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + String presentation = sdJwtVP.present(null, keyBindingClaims, + TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from settings + presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getHolderVerifierContext()); + + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + } + + @Test(expected = VerificationException.class) + public void testS6_2_PresentationNegative() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + String presentation = sdJwtVP.present(null, keyBindingClaims, + TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + + // Verify with wrong public key from settings (iisuer) + presenteSdJwtVP.getKeyBindingJWT().get().verifySignature(TestSettings.getInstance().getIssuerVerifierContext()); + } + + @Test + public void testS6_2_PresentationPartialDisclosure() throws VerificationException { + String jwsType = "vc+sd-jwt"; + String sdJwtVPString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt"); + SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtVPString); + JsonNode keyBindingClaims = TestUtils.readClaimSet(getClass(), "sdjwt/s6.2-key-binding-claims.json"); + // disclose only the given_name + String presentation = sdJwtVP.present(Arrays.asList("jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4"), + keyBindingClaims, TestSettings.getInstance().getHolderSignerContext(), jwsType); + + SdJwtVP presenteSdJwtVP = SdJwtVP.of(presentation); + assertTrue(presenteSdJwtVP.getKeyBindingJWT().isPresent()); + + // Verify with public key from cnf claim + presenteSdJwtVP.getKeyBindingJWT().get() + .verifySignature(TestSettings.verifierContextFrom(presenteSdJwtVP.getCnfClaim(), "ES256")); + } + +} diff --git a/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/TestCompareSdJwt.java b/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/TestCompareSdJwt.java new file mode 100644 index 0000000..d741c8f --- /dev/null +++ b/src/test/java/com/adorsys/ssi/sdjwt/sdjwtvp/TestCompareSdJwt.java @@ -0,0 +1,101 @@ + +package com.adorsys.ssi.sdjwt.sdjwtvp; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.common.util.Base64Url; +import org.keycloak.sdjwt.IssuerSignedJWT; +import org.keycloak.sdjwt.SdJwtUtils; +import org.keycloak.sdjwt.vp.SdJwtVP; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * This class will try to test conformity to the spec by comparing json objects. + * + * + * We are facing the situation that: + * - json produced are not normalized. But we can compare them by natching their + * content once loaded into a json object. + * - ecdsa signature contains random component. We can't compare them directly. + * Even if we had the same input byte + * - The no rationale for ordering the disclosures. So we can only make sure + * each of them is present and that the json content matches. + * + * Warning: in orther to produce the same disclosure strings and hashes like in + * the spect, i had to produce + * the same print. This is by no way reliable enougth to be used to test + * conformity to the spec. + * + * @author Francis Pouatcha + */ +public class TestCompareSdJwt { + + public static void compare(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) { + try { + compareIssuerSignedJWT(expectedSdJwt.getIssuerSignedJWT(), actualSdJwt.getIssuerSignedJWT()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + compareDisclosures(expectedSdJwt, actualSdJwt); + + } + + private static void compareIssuerSignedJWT(IssuerSignedJWT e, IssuerSignedJWT a) + throws JsonMappingException, JsonProcessingException { + + assertEquals(e.getPayload(), a.getPayload()); + + List expectedJwsStrings = Arrays.asList(e.getJwsString().split("\\.")); + List actualJwsStrings = Arrays.asList(a.getJwsString().split("\\.")); + + // compare json content of header + assertEquals(toJsonNode(expectedJwsStrings.get(0)), toJsonNode(actualJwsStrings.get(0))); + + // compare payload + assertEquals(toJsonNode(expectedJwsStrings.get(1)), toJsonNode(actualJwsStrings.get(1))); + + // We wont compare signatures. + } + + private static void compareDisclosures(SdJwtVP expectedSdJwt, SdJwtVP actualSdJwt) { + Set expectedDisclosures = expectedSdJwt.getDisclosuresString().stream() + .map(TestCompareSdJwt::toJsonNode) + .collect(Collectors.toSet()); + Set actualDisclosures = expectedSdJwt.getDisclosuresString().stream() + .map(TestCompareSdJwt::toJsonNode) + .collect(Collectors.toSet()); + + assertEquals(expectedDisclosures.size(), actualDisclosures.size()); + + boolean foundEqualPair = false; + for (JsonNode a : expectedDisclosures) { + for (JsonNode b : actualDisclosures) { + if (a.equals(b)) { + foundEqualPair = true; + break; + } + } + } + + assertTrue("The set should contain equal elements", foundEqualPair); + } + + private static JsonNode toJsonNode(String base64EncodedString) { + try { + return SdJwtUtils.mapper.readTree(Base64Url.decode(base64EncodedString)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/test/resources/sdjwt/a1.example2-address-payload.json b/src/test/resources/sdjwt/a1.example2-address-payload.json new file mode 100644 index 0000000..c0c8709 --- /dev/null +++ b/src/test/resources/sdjwt/a1.example2-address-payload.json @@ -0,0 +1,12 @@ +{ + "_sd": [ + "IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8", + "Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo", + "QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM", + "UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg", + "cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo", + "oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA", + "qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74", + "uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4" + ] +} diff --git a/src/test/resources/sdjwt/a1.example2-holder-claims.json b/src/test/resources/sdjwt/a1.example2-holder-claims.json new file mode 100644 index 0000000..2442cee --- /dev/null +++ b/src/test/resources/sdjwt/a1.example2-holder-claims.json @@ -0,0 +1,14 @@ +{ + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "given_name": "太郎", + "family_name": "山田", + "email": "\"unusual email address\"@example.jp", + "phone_number": "+81-80-1234-5678", + "address": { + "street_address": "東京都港区芝公園4丁目2−8", + "locality": "東京都", + "region": "港区", + "country": "JP" + }, + "birthdate": "1940-01-01" +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/a1.example2-issuer-claims.json b/src/test/resources/sdjwt/a1.example2-issuer-claims.json new file mode 100644 index 0000000..27e72e2 --- /dev/null +++ b/src/test/resources/sdjwt/a1.example2-issuer-claims.json @@ -0,0 +1,5 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000 +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/a1.example2-issuer-payload.json b/src/test/resources/sdjwt/a1.example2-issuer-payload.json new file mode 100644 index 0000000..1ae5d7e --- /dev/null +++ b/src/test/resources/sdjwt/a1.example2-issuer-payload.json @@ -0,0 +1,28 @@ +{ + "_sd": [ + "9hf5niUdeWrPmaU5mz727OELoKHX5TDZjrBVHCVzqcg", + "Kfv8UXTNDG2NWPv6CtT5QAa-w5-ugOfICaoap474crk", + "Kuet1yAa0HIQvYnOVd59hcViO9Ug6J2kSfqYRBeowvE", + "MMldOFFzB2d0umlmpTIaGerhWdU_PpYfLvKhh_f_9aY", + "X6ZAYOII2vPN40V7xExZwVwz7yRmLNcVwt5DL8RLv4g", + "ihDxP1pJ59-iRb-aft25j3cqC1ShChhO_sWC02gVUGw", + "s0BKYsLWxQQeU8tVlltM7MKsIRTrEIa1PkJmqxBBf5U", + "vg70gfzXO8HR7ERDkL46S6Ior1ey0DvZoEUHupJwoxc" + ], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "address": { + "_sd": [ + "IZOyJn0D17aK845iZ8i5hDloSkartiUlvp_hKjNbAt8", + "Lsx_tw-UwEZ_HsK8QcIDkCn5Wvfe5BmvcnWQB6ikqqo", + "QNQd3_y8o6qtdJguQDuA3zdfdYz-WgLSaje06s2UmWM", + "UWzvCBURYx4dSkeBCptgLtudFbLgnJoBgmaHB-76lOg", + "cx4toEb1qARkAf8NuD0ATk3oM6m8a0q8nAVFDtBdfoo", + "oUuE90DUCx3Xu_H5zQMBEqAdbMrlAZ7QoK5zIJ_B1mA", + "qd2G5TGH-6M1qNy8ouhXfsE7U6vWDOnp2FUvovAaW74", + "uNHoWYhXsZhVJCNE2Dqy-zqt7t69gJKy5QaFv7GrMX4" + ] + }, + "_sd_alg": "sha-256" +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/a1.example2-sdjwt.txt b/src/test/resources/sdjwt/a1.example2-sdjwt.txt new file mode 100644 index 0000000..23537c6 --- /dev/null +++ b/src/test/resources/sdjwt/a1.example2-sdjwt.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiOWhmNW5pVWRlV3JQbWFVNW16NzI3T0VMb0tIWDVURFpqckJWSENWenFjZyIsIktmdjhVWFROREcyTldQdjZDdFQ1UUFhLXc1LXVnT2ZJQ2FvYXA0NzRjcmsiLCJLdWV0MXlBYTBISVF2WW5PVmQ1OWhjVmlPOVVnNkoya1NmcVlSQmVvd3ZFIiwiTU1sZE9GRnpCMmQwdW1sbXBUSWFHZXJoV2RVX1BwWWZMdktoaF9mXzlhWSIsIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCJpaER4UDFwSjU5LWlSYi1hZnQyNWozY3FDMVNoQ2hoT19zV0MwMmdWVUd3IiwiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSIsInZnNzBnZnpYTzhIUjdFUkRrTDQ2UzZJb3IxZXkwRHZab0VVSHVwSndveGMiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJhZGRyZXNzIjp7Il9zZCI6WyJJWk95Sm4wRDE3YUs4NDVpWjhpNWhEbG9Ta2FydGlVbHZwX2hLak5iQXQ4IiwiTHN4X3R3LVV3RVpfSHNLOFFjSURrQ241V3ZmZTVCbXZjbldRQjZpa3FxbyIsIlFOUWQzX3k4bzZxdGRKZ3VRRHVBM3pkZmRZei1XZ0xTYWplMDZzMlVtV00iLCJVV3p2Q0JVUll4NGRTa2VCQ3B0Z0x0dWRGYkxnbkpvQmdtYUhCLTc2bE9nIiwiY3g0dG9FYjFxQVJrQWY4TnVEMEFUazNvTTZtOGEwcThuQVZGRHRCZGZvbyIsIm9VdUU5MERVQ3gzWHVfSDV6UU1CRXFBZGJNcmxBWjdRb0s1eklKX0IxbUEiLCJxZDJHNVRHSC02TTFxTnk4b3VoWGZzRTdVNnZXRE9ucDJGVXZvdkFhVzc0IiwidU5Ib1dZaFhzWmhWSkNORTJEcXktenF0N3Q2OWdKS3k1UWFGdjdHck1YNCJdfSwiaXNzIjoiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCJpYXQiOjE2ODMwMDAwMDAsImV4cCI6MTg4MzAwMDAwMH0.MEQCIDiR0hG5E-jCC6YEr1nrTJSOwIn7FL8FmQWhJfFgkjRrAiAPPnEfgnBRiad2RyfNjIx6UzGV2TP0SYLhNTm6syGMjw~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAi5aSq6YOOIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIuWxseeUsCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0~WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/s3.3-holder-claims.json b/src/test/resources/sdjwt/s3.3-holder-claims.json new file mode 100644 index 0000000..f953b06 --- /dev/null +++ b/src/test/resources/sdjwt/s3.3-holder-claims.json @@ -0,0 +1,17 @@ +{ + "vct": "https://credentials.example.com/identity_credential", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "is_over_18": true, + "is_over_21": true, + "is_over_65": true +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s3.3-issuer-claims.json b/src/test/resources/sdjwt/s3.3-issuer-claims.json new file mode 100644 index 0000000..bf99ac2 --- /dev/null +++ b/src/test/resources/sdjwt/s3.3-issuer-claims.json @@ -0,0 +1,14 @@ +{ + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "vct": "https://credentials.example.com/identity_credential", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + } +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s3.3-issuer-payload.json b/src/test/resources/sdjwt/s3.3-issuer-payload.json new file mode 100644 index 0000000..4c5c737 --- /dev/null +++ b/src/test/resources/sdjwt/s3.3-issuer-payload.json @@ -0,0 +1,26 @@ +{ + "_sd": [ + "09vKrJMOlyTWM0sjpu_pdOBVBQ2M1y3KhpH515nXkpY", + "2rsjGbaC0ky8mT0pJrPioWTq0_daw1sX76poUlgCwbI", + "EkO8dhW0dHEJbvUHlE_VCeuC9uRELOieLZhh7XbUTtA", + "IlDzIKeiZdDwpqpK6ZfbyphFvz5FgnWa-sN6wqQXCiw", + "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", + "PorFbpKuVu6xymJagvkFsFXAbRoc2JGlAUA2BA4o7cI", + "TGf4oLbgwd5JQaHyKVQZU9UdGE0w5rtDsrZzfUaomLo", + "jdrTE8YcbY4EifugihiAe_BPekxJQZICeiUQwY9QqxI", + "jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4" + ], + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + "vct": "https://credentials.example.com/identity_credential", + "_sd_alg": "sha-256", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + } +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt b/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt new file mode 100644 index 0000000..9416924 --- /dev/null +++ b/src/test/resources/sdjwt/s3.3-unsecured-sd-jwt.txt @@ -0,0 +1,28 @@ +eyJhbGciOiAiRVMyNTYiLCAia2lkIjogImRvYy1zaWduZXItMDUtMjUtMjAyMiIsICJ0 +eXAiOiAidmMrc2Qtand0In0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9C +VkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9k +YXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9p +ZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNO +NndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQ +WWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJ +IiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAi +amRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5 +eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAi +aHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4 +cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxl +LmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJj +bmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRD +QUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJa +eGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.YHjaS +waBy-6hBYBre1F1ehiHNp69F9jnP2Hve3g0gNTzG_6GxV-E9rPR5m_CCo1SgDk0GaE5S +II6FBprkwDP-Q~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLC +AiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgI +kRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VA +ZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251b +WJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIi +wgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2 +FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOi +AiVVMifV0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOT +QwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLC +B0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnV +lXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.1-holder-claims.json b/src/test/resources/sdjwt/s6.1-holder-claims.json new file mode 100644 index 0000000..38d3eb0 --- /dev/null +++ b/src/test/resources/sdjwt/s6.1-holder-claims.json @@ -0,0 +1,20 @@ +{ + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "phone_number_verified": true, + "address": { + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US" + }, + "birthdate": "1940-01-01", + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.1-issued-payload.txt b/src/test/resources/sdjwt/s6.1-issued-payload.txt new file mode 100644 index 0000000..12bd739 --- /dev/null +++ b/src/test/resources/sdjwt/s6.1-issued-payload.txt @@ -0,0 +1,29 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb +IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ +akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL +dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1 +SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB +TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2 +Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr +b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn +bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu +Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog +InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15 +VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1 +ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog +InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y +NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH +ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG +MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK +wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgI +mdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZh +bWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWl +sIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhR +IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4Z +TQxMmExMDhpcm9BIiwgInBob25lX251bWJlcl92ZXJpZmllZCIsIHRydWVd~WyJBSngt +MDk1VlBycFR0TjRRTU9xUk9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjog +IjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFu +eXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZR +IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJHMDJOU3JRZmpGWFE3SW8wOXN5 +YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ~WyJsa2x4RjVqTVlsR1RQVW92T +U5JdkNBIiwgIlVTIl0~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json b/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json new file mode 100644 index 0000000..dc441c0 --- /dev/null +++ b/src/test/resources/sdjwt/s6.1-issuer-payload-decoy-array-ellement.json @@ -0,0 +1,19 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + { "...": "mXYRA4kcMm9hHUX-dCc44jKpyrNiEtJo2IqLk5YzRik" }, + { "...": "XkluhXNRk-Gmh8zBHo4Ad3drmukEbmm4CECMCefdG24" }, + { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } + ] +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json b/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json new file mode 100644 index 0000000..5dfd804 --- /dev/null +++ b/src/test/resources/sdjwt/s6.1-issuer-payload-udisclosed-array-ellement.json @@ -0,0 +1,18 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + "US", + { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } + ] +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.1-issuer-payload.json b/src/test/resources/sdjwt/s6.1-issuer-payload.json new file mode 100644 index 0000000..45e7510 --- /dev/null +++ b/src/test/resources/sdjwt/s6.1-issuer-payload.json @@ -0,0 +1,18 @@ +{ + "_sd": [ + "cLaGEUHQDOcM0GyUsKtDLZFhKy59prqFdilqto_z-wI", + "dHEndXpEuP1u9UVDyZ_4SrShHU5UaAcr-plj7T6ht88", + "fF0-qMHsHyamOQ82bOGciYLSLtqhxHsxBDVfVRZfosM", + "sitC0MiJMDj2RPcuh4ZYERBrirwgf58iG3b1QV8G9TM" + ], + "_sd_alg": "sha-256", + "sub": "user_42", + "given_name": "John", + "family_name": "Doe", + "phone_number_verified": true, + "updated_at": 1570000000, + "nationalities": [ + "US", + "DE" + ] +} \ No newline at end of file diff --git a/src/test/resources/sdjwt/s6.2-key-binding-claims.json b/src/test/resources/sdjwt/s6.2-key-binding-claims.json new file mode 100644 index 0000000..7d9d320 --- /dev/null +++ b/src/test/resources/sdjwt/s6.2-key-binding-claims.json @@ -0,0 +1,5 @@ +{ + "nonce": "1234567890", + "aud": "https://verifier.example.org", + "iat": 1702315679 +} diff --git a/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt b/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt new file mode 100644 index 0000000..7553c04 --- /dev/null +++ b/src/test/resources/sdjwt/s6.2-presented-sdjwtvp.txt @@ -0,0 +1,23 @@ +eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBb +IkNyUWU3UzVrcUJBSHQtbk1ZWGdjNmJkdDJTSDVhVFkxc1VfTS1QZ2tqUEkiLCAiSnpZ +akg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBL +dVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1 +SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiWFFfM2tQS3QxWHlYN0tB +TmtxVlI2eVoyVmE1TnJQSXZQWWJ5TXZSS0JNTSIsICJYekZyendzY002R242Q0pEYzZ2 +Vks4QmtNbmZHOHZPU0tmcFBJWmRBZmRFIiwgImdiT3NJNEVkcTJ4Mkt3LXc1d1BFemFr +b2I5aFYxY1JEMEFUTjNvUUw5Sk0iLCAianN1OXlWdWx3UVFsaEZsTV8zSmx6TWFTRnpn +bGhRRzBEcGZheVF3TFVLNCJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUu +Y29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAic3ViIjog +InVzZXJfNDIiLCAibmF0aW9uYWxpdGllcyI6IFt7Ii4uLiI6ICJwRm5kamtaX1ZDem15 +VGE2VWpsWm8zZGgta284YUlLUWM5RGxHemhhVllvIn0sIHsiLi4uIjogIjdDZjZKa1B1 +ZHJ5M2xjYndIZ2VaOGtoQXYxVTFPU2xlclAwVmtCSnJXWjAifV0sICJfc2RfYWxnIjog +InNoYS0yNTYiLCAiY25mIjogeyJqd2siOiB7Imt0eSI6ICJFQyIsICJjcnYiOiAiUC0y +NTYiLCAieCI6ICJUQ0FFUjE5WnZ1M09IRjRqNFc0dmZTVm9ISVAxSUxpbERsczd2Q2VH +ZW1jIiwgInkiOiAiWnhqaVdXYlpNUUdIVldLVlE0aGJTSWlyc1ZmdWVjQ0U2dDRqVDlG +MkhaUSJ9fX0.TpMrn_a_ZSedw-jC2XAD74GBXhB5t35pQK1-MYiYnibXr-O4sumCK-BK +wIseZm6maVGhglKOs6nwN5ooUF3uiQ~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgI +mZhbWlseV9uYW1lIiwgIkRvZSJd~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImFk +ZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5 +IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMi +fV0~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd +~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/s7-holder-claims.json b/src/test/resources/sdjwt/s7-holder-claims.json new file mode 100644 index 0000000..532e51f --- /dev/null +++ b/src/test/resources/sdjwt/s7-holder-claims.json @@ -0,0 +1,9 @@ +{ + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "street_address": "Schulstr. 12", + "locality": "Schulpforta", + "region": "Sachsen-Anhalt", + "country": "DE" + } +} diff --git a/src/test/resources/sdjwt/s7-issuer-claims.json b/src/test/resources/sdjwt/s7-issuer-claims.json new file mode 100644 index 0000000..399212e --- /dev/null +++ b/src/test/resources/sdjwt/s7-issuer-claims.json @@ -0,0 +1,5 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000 +} diff --git a/src/test/resources/sdjwt/s7.1-issuer-payload.json b/src/test/resources/sdjwt/s7.1-issuer-payload.json new file mode 100644 index 0000000..fb78078 --- /dev/null +++ b/src/test/resources/sdjwt/s7.1-issuer-payload.json @@ -0,0 +1,8 @@ +{ + "_sd": ["fOBUSQvo46yQO-wRwXBcGqvnbKIueISEL961_Sjd4do"], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "_sd_alg": "sha-256" +} diff --git a/src/test/resources/sdjwt/s7.2-issuer-payload.json b/src/test/resources/sdjwt/s7.2-issuer-payload.json new file mode 100644 index 0000000..18b4808 --- /dev/null +++ b/src/test/resources/sdjwt/s7.2-issuer-payload.json @@ -0,0 +1,15 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "_sd": [ + "6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0", + "9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM", + "KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88", + "WN9r9dCBJ8HTCsS2jKASxTjEyW5m5x65_Z_2ro2jfXM" + ] + }, + "_sd_alg": "sha-256" +} diff --git a/src/test/resources/sdjwt/s7.2b-issuer-payload.json b/src/test/resources/sdjwt/s7.2b-issuer-payload.json new file mode 100644 index 0000000..57e0380 --- /dev/null +++ b/src/test/resources/sdjwt/s7.2b-issuer-payload.json @@ -0,0 +1,15 @@ +{ + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "address": { + "_sd": [ + "6vh9bq-zS4GKM_7GpggVbYzzu6oOGXrmNVGPHP75Ud0", + "9gjVuXtdFROCgRrtNcGUXmF65rdezi_6Er_j76kmYyM", + "KURDPh4ZC19-3tiz-Df39V8eidy1oV3a3H1Da2N0g88" + ], + "country": "DE" + }, + "_sd_alg": "sha-256" +} diff --git a/src/test/resources/sdjwt/s7.3-issuer-payload.json b/src/test/resources/sdjwt/s7.3-issuer-payload.json new file mode 100644 index 0000000..976f8f0 --- /dev/null +++ b/src/test/resources/sdjwt/s7.3-issuer-payload.json @@ -0,0 +1,8 @@ +{ + "_sd": ["HvrKX6fPV0v9K_yCVFBiLFHsMaxcD_114Em6VT8x1lg"], + "iss": "https://issuer.example.com", + "iat": 1683000000, + "exp": 1883000000, + "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + "_sd_alg": "sha-256" +} diff --git a/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt b/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt new file mode 100644 index 0000000..b35df40 --- /dev/null +++ b/src/test/resources/sdjwt/s7.3-sdjwt+ghost.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIuadseS6rOmDvea4r-WMuuiKneWFrOWcku-8lOS4geebru-8kuKIku-8mCJd~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIuadseS6rOmDvSJd~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICLmuK_ljLoiXQ~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/s7.3-sdjwt.txt b/src/test/resources/sdjwt/s7.3-sdjwt.txt new file mode 100644 index 0000000..c980125 --- /dev/null +++ b/src/test/resources/sdjwt/s7.3-sdjwt.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCIgOiAidmMrc2Qtand0Iiwia2lkIiA6ICJkb2Mtc2lnbmVyLTA1LTI1LTIwMjIifQ.eyJfc2QiOlsiSHZyS1g2ZlBWMHY5S195Q1ZGQmlMRkhzTWF4Y0RfMTE0RW02VlQ4eDFsZyJdLCJfc2RfYWxnIjoic2hhLTI1NiIsInN1YiI6IjZjNWMwYTQ5LWI1ODktNDMxZC1iYWU3LTIxOTEyMmE5ZWMyYyIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNjgzMDAwMDAwLCJleHAiOjE4ODMwMDAwMDB9.MEQCICde3GkeuNWixUiD3zk5F9OMGD2HJW6Lmo4waWkVXDddAiByEPMrOn8aE9Tf33_J3SIiVBEhQNthU58O7D0Y1Xywcg~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN0cmVldF9hZGRyZXNzIiwgIlNjaHVsc3RyLiAxMiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImxvY2FsaXR5IiwgIlNjaHVscGZvcnRhIl0~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInJlZ2lvbiIsICJTYWNoc2VuLUFuaGFsdCJd~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImNvdW50cnkiLCAiREUiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7Il9zZCI6IFsiNnZoOWJxLXpTNEdLTV83R3BnZ1ZiWXp6dTZvT0dYcm1OVkdQSFA3NVVkMCIsICI5Z2pWdVh0ZEZST0NnUnJ0TmNHVVhtRjY1cmRlemlfNkVyX2o3NmttWXlNIiwgIktVUkRQaDRaQzE5LTN0aXotRGYzOVY4ZWlkeTFvVjNhM0gxRGEyTjBnODgiLCAiV045cjlkQ0JKOEhUQ3NTMmpLQVN4VGpFeVc1bTV4NjVfWl8ycm8yamZYTSJdfV0~ \ No newline at end of file diff --git a/src/test/resources/sdjwt/test-settings.json b/src/test/resources/sdjwt/test-settings.json new file mode 100644 index 0000000..1db5b9d --- /dev/null +++ b/src/test/resources/sdjwt/test-settings.json @@ -0,0 +1,29 @@ +{ + "identifiers": { + "issuer": "https://example.com/issuer", + "verifier": "https://example.com/verifier" + }, + "key_settings": { + "key_size": 256, + "kty": "EC", + "issuer_key": { + "kty": "EC", + "d": "Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g", + "crv": "P-256", + "x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ", + "y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" + }, + "holder_key": { + "kty": "EC", + "d": "5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I", + "crv": "P-256", + "x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc", + "y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ" + } + }, + "key_binding_nonce": "1234567890", + "expiry_seconds": 86400000, + "random_seed": 0, + "iat": 1683000000, + "exp": 1883000000 +} diff --git a/src/test/resources/sdjwt/test-settings.yml b/src/test/resources/sdjwt/test-settings.yml new file mode 100644 index 0000000..5795afb --- /dev/null +++ b/src/test/resources/sdjwt/test-settings.yml @@ -0,0 +1,32 @@ +# from: https://github.com/openwallet-foundation-labs/sd-jwt-python/blob/main/src/sd_jwt/utils/demo_settings.yml +identifiers: + issuer: "https://example.com/issuer" + verifier: "https://example.com/verifier" + +key_settings: + key_size: 256 + + kty: EC + + issuer_key: + kty: EC + d: Ur2bNKuBPOrAaxsRnbSH6hIhmNTxSGXshDSUD1a1y7g + crv: P-256 + x: b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ + y: Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8 + + holder_key: + kty: EC + d: 5K5SCos8zf9zRemGGUl6yfok-_NiiryNZsvANWMhF-I + crv: P-256 + x: TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc + y: ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ + +key_binding_nonce: "1234567890" + +expiry_seconds: 86400000 # 1000 days + +random_seed: 0 + +iat: 1683000000 # Tue May 02 2023 04:00:00 GMT+0000 +exp: 1883000000 # Sat Sep 01 2029 23:33:20 GMT+0000 \ No newline at end of file