SdJwtVPTest.java
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sdjwt.sdjwtvp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.common.VerificationException;
import org.keycloak.rule.CryptoInitRule;
import org.keycloak.sdjwt.DisclosureSpec;
import org.keycloak.sdjwt.IssuerSignedJWT;
import org.keycloak.sdjwt.SdJwt;
import org.keycloak.sdjwt.TestSettings;
import org.keycloak.sdjwt.TestUtils;
import org.keycloak.sdjwt.vp.SdJwtVP;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:francis.pouatcha@adorsys.com">Francis Pouatcha</a>
*/
public abstract class SdJwtVPTest {
@ClassRule
public static CryptoInitRule cryptoInitRule = new CryptoInitRule();
// 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"));
}
@Test
public void testOf_validInput() {
String sdJwtString = TestUtils.readFileAsString(getClass(), "sdjwt/s6.2-presented-sdjwtvp.txt");
SdJwtVP sdJwtVP = SdJwtVP.of(sdJwtString);
assertNotNull(sdJwtVP);
assertEquals(4, sdJwtVP.getDisclosures().size());
}
@Test
public void testOf_MalformedSdJwt_ThrowsIllegalArgumentException() {
// Given
String malformedSdJwt = "issuer-signed-jwt";
// When & Then
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> SdJwtVP.of(malformedSdJwt));
assertEquals("SD-JWT is malformed, expected to contain a '~'", exception.getMessage());
}
}