From af01d93aefdb3c99f81537af2eecdc7ee5aef15f Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Thu, 9 Oct 2025 14:45:56 +0700 Subject: [PATCH 1/2] feat: verify registries and credentials --- .../credentials/Credentials.java | 29 +- .../credentialing/registries/Registries.java | 25 + .../signify/e2e/VerifyCredentialTest.java | 633 ++++++++++++++++++ 3 files changed, 685 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java index bc434d1..0208192 100644 --- a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java @@ -56,7 +56,9 @@ public Optional get(String said) throws IOException, InterruptedExceptio * * @param said - SAID of the credential * @param includeCESR - Optional flag export the credential in CESR format - * @return Optional containing the credential if found, or empty if not found + * @return Optional containing the credential if found, or empty if not found. + * Returns String (raw CESR text) when includeCESR=true, + * or Object (parsed JSON) when includeCESR=false */ public Optional get(String said, boolean includeCESR) throws IOException, InterruptedException, LibsodiumException { final String path = "/credentials/" + said; @@ -75,7 +77,7 @@ public Optional get(String said, boolean includeCESR) throws IOException return Optional.empty(); } - return Optional.of(Utils.fromJson(response.body(), Object.class)); + return Optional.of(includeCESR ? response.body() : Utils.fromJson(response.body(), Object.class)); } /** @@ -256,4 +258,27 @@ public RevokeCredentialResult revoke(String name, String said, String datetime) return new RevokeCredentialResult(new Serder(ixn), new Serder(rev), op); } + + /** + * Verify a credential and issuing event + * + * @param acdc ACDC to process and verify + * @param iss Issuing event for ACDC in TEL + * @param atc Optional attachment string to be verified against the credential + * @return Operation containing the verification result + */ + public Operation verify(Serder acdc, Serder iss, String atc) throws IOException, InterruptedException, LibsodiumException { + final String path = "/credentials/verify"; + final String method = "POST"; + + Map body = new LinkedHashMap<>(); + body.put("acdc", acdc.getKed()); + body.put("iss", iss.getKed()); + if (atc != null && !atc.isEmpty()) { + body.put("atc", atc); + } + + HttpResponse response = this.client.fetch(path, method, body); + return Operation.fromObject(Utils.fromJson(response.body(), Map.class)); + } } diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java index 41626b5..3ef5839 100644 --- a/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java @@ -2,6 +2,7 @@ import org.cardanofoundation.signify.app.clienting.SignifyClient; import org.cardanofoundation.signify.app.coring.Coring; +import org.cardanofoundation.signify.app.coring.Operation; import org.cardanofoundation.signify.app.habery.TraitCodex; import org.cardanofoundation.signify.cesr.Keeping; import org.cardanofoundation.signify.cesr.Serder; @@ -165,4 +166,28 @@ public Object rename(String name, String registryName, String newName) throws IO HttpResponse response = this.client.fetch(path, method, data); return Utils.fromJson(response.body(), Object.class); } + + /** + * Verify a registry with optional attachment + * + * @param vcp the VCP (Verifiable Credential Protocol) data to verify + * @param atc the optional attachment data (metadata) + * @return Operation containing the verification result + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + * @throws LibsodiumException if a sodium exception occurs + */ + public Operation verify(Serder vcp, String atc) throws IOException, InterruptedException, LibsodiumException { + final String path = "/registries/verify"; + final String method = "POST"; + + Map body = new LinkedHashMap<>(); + body.put("vcp", vcp.getKed()); + if (atc != null && !atc.isEmpty()) { + body.put("atc", atc); + } + + HttpResponse response = this.client.fetch(path, method, body); + return Operation.fromObject(Utils.fromJson(response.body(), Map.class)); + } } diff --git a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java new file mode 100644 index 0000000..3760132 --- /dev/null +++ b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java @@ -0,0 +1,633 @@ +package org.cardanofoundation.signify.e2e; + +import org.cardanofoundation.signify.app.clienting.SignifyClient; +import org.cardanofoundation.signify.app.coring.Operation; +import org.cardanofoundation.signify.app.credentialing.credentials.*; +import org.cardanofoundation.signify.app.credentialing.registries.CreateRegistryArgs; +import org.cardanofoundation.signify.app.credentialing.registries.RegistryResult; +import org.cardanofoundation.signify.cesr.Serder; +import org.cardanofoundation.signify.cesr.util.Utils; +import org.cardanofoundation.signify.e2e.utils.ResolveEnv; +import org.cardanofoundation.signify.e2e.utils.TestSteps; +import org.cardanofoundation.signify.e2e.utils.TestUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.security.DigestException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.cardanofoundation.signify.e2e.utils.TestUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +public class VerifyCredentialTest extends BaseIntegrationTest { + private ResolveEnv.EnvironmentConfig env = ResolveEnv.resolveEnvironment(null); + private String QVI_SCHEMA_SAID = "EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao"; + private String LE_SCHEMA_SAID = "ENPXp1vQzRF6JwIuS-mp2U8Uf1MoADoP_GqQ62VsDZWY"; + private String vLEIServerHostUrl = env.vleiServerUrl() + "/oobi"; + private String QVI_SCHEMA_URL = vLEIServerHostUrl + "/" + QVI_SCHEMA_SAID; + private String LE_SCHEMA_URL = vLEIServerHostUrl + "/" + LE_SCHEMA_SAID; + TestSteps testSteps = new TestSteps(); + + private static SignifyClient issuerClient, verifierClient, holderClient, legalEntityClient; + private TestUtils.Aid issuerAid, verifierAid, holderAid, legalEntityAid; + + // Global variables to store QVI credential components + private static Map vcpEvent; + private static String vcpAttachment; + private static Map issEvent; + private static String issAttachment; + private static Map acdcEvent; + private static String qviCredentialId; + + // Global variables to store LE (chained) credential components + private static Map leVcpEvent; + private static String leVcpAttachment; + private static Map leIssEvent; + private static String leIssAttachment; + private static Map leAcdcEvent; + private static String leCredentialId; + private static String leCredentialCesr; + + @BeforeAll + public static void getClients() throws Exception { + List clients = getOrCreateClientsAsync(4); + issuerClient = clients.get(0); + verifierClient = clients.get(1); + holderClient = clients.get(2); + legalEntityClient = clients.get(3); + } + + @BeforeEach + public void getAid() throws Exception { + List aids = createAidAsync( + new CreateAidArgs(issuerClient, "issuer"), + new CreateAidArgs(verifierClient, "verifier"), + new CreateAidArgs(holderClient, "holder"), + new CreateAidArgs(legalEntityClient, "legal-entity") + ); + issuerAid = aids.get(0); + verifierAid = aids.get(1); + holderAid = aids.get(2); + legalEntityAid = aids.get(3); + } + + @BeforeEach + public void getContact() { + getOrCreateContactAsync( + new GetOrCreateContactArgs(issuerClient, "verifier", verifierAid.oobi), + new GetOrCreateContactArgs(issuerClient, "holder", holderAid.oobi), + new GetOrCreateContactArgs(verifierClient, "issuer", issuerAid.oobi), + new GetOrCreateContactArgs(verifierClient, "holder", holderAid.oobi), + new GetOrCreateContactArgs(holderClient, "issuer", issuerAid.oobi), + new GetOrCreateContactArgs(holderClient, "legal-entity", legalEntityAid.oobi), + new GetOrCreateContactArgs(holderClient, "verifier", verifierAid.oobi), + new GetOrCreateContactArgs(legalEntityClient, "holder", holderAid.oobi), + new GetOrCreateContactArgs(legalEntityClient, "issuer", issuerAid.oobi) + ); + System.out.println("Created contact successfully"); + } + + @AfterAll + public static void cleanup() throws Exception { + List clients = Arrays.asList( + issuerClient, + verifierClient, + holderClient, + legalEntityClient + ); + assertOperations(clients); + assertNotifications(clients); + } + + @Test + @SuppressWarnings("unchecked") + public void verify_credential_workflow() throws Exception { + testSteps.step("Resolve schema oobis", () -> { + resolveOobisAsync( + new ResolveOobisArgs(issuerClient, QVI_SCHEMA_URL, null), + new ResolveOobisArgs(issuerClient, LE_SCHEMA_URL, null), + new ResolveOobisArgs(verifierClient, QVI_SCHEMA_URL, null), + new ResolveOobisArgs(verifierClient, LE_SCHEMA_URL, null), + new ResolveOobisArgs(verifierClient, issuerAid.oobi, null), + new ResolveOobisArgs(holderClient, QVI_SCHEMA_URL, null), + new ResolveOobisArgs(holderClient, LE_SCHEMA_URL, null), + new ResolveOobisArgs(legalEntityClient, LE_SCHEMA_URL, null) + ); + }); + + HashMap registry = testSteps.step("Create registry", () -> { + String registryName = "vLEI-test-registry"; + HashMap registryData = new HashMap<>(); + + CreateRegistryArgs registryArgs = CreateRegistryArgs.builder().build(); + registryArgs.setName(issuerAid.name); + registryArgs.setRegistryName(registryName); + try { + RegistryResult regResult = issuerClient.registries().create(registryArgs); + waitOperation(issuerClient, regResult.op()); + + Object registries = issuerClient.registries().list(issuerAid.name); + List> registriesList = castObjectToListMap(registries); + + registryData.put("name", registriesList.getFirst().get("name").toString()); + registryData.put("regk", registriesList.getFirst().get("regk").toString()); + + assertEquals(1, registriesList.size()); + assertEquals(registryName, registryData.get("name")); + + return registryData; + } catch (IOException | InterruptedException | DigestException e) { + throw new RuntimeException(e); + } + }); + + qviCredentialId = testSteps.step("Issue QVI credential and extract components", () -> { + Map vcdata = new HashMap<>(); + vcdata.put("LEI", "5493001KJTIIGC8Y1R17"); + + CredentialData.CredentialSubject a = CredentialData.CredentialSubject.builder().build(); + a.setI(holderAid.prefix); // Credential subject is holder + a.setAdditionalProperties(vcdata); + + CredentialData cData = CredentialData.builder().build(); + cData.setRi(registry.get("regk").toString()); + cData.setS(QVI_SCHEMA_SAID); + cData.setA(a); + + try { + IssueCredentialResult issResult = issuerClient.credentials().issue(issuerAid.name, cData); + waitOperation(issuerClient, issResult.getOp()); + String credId = issResult.getAcdc().getKed().get("d").toString(); + + // Get the credential with CESR format to extract components + Optional credentialOpt = issuerClient.credentials().get(credId, true); + String credentialCesr = (String) credentialOpt.get(); + + // Parse CESR data to extract VCP, ISS, and ACDC events + List> cesrData = parseCESRData(credentialCesr); + + for (Map eventData : cesrData) { + Map event = (Map) eventData.get("event"); + + // Check for event type + Object eventTypeObj = event.get("t"); + if (eventTypeObj != null) { + String eventType = eventTypeObj.toString(); + switch (eventType) { + case "vcp": + vcpEvent = event; + vcpAttachment = (String) eventData.get("atc"); + break; + case "iss": + issEvent = event; + issAttachment = (String) eventData.get("atc"); + break; + } + } else { + // Check if this is an ACDC (credential data) without "t" field + if (event.containsKey("s") && event.containsKey("a") && event.containsKey("i")) { + Object schemaObj = event.get("s"); + if (schemaObj != null && QVI_SCHEMA_SAID.equals(schemaObj.toString())) { + acdcEvent = event; + // System.out.println("Extracted ACDC event"); + } + } + } + } + + // Verify all components were extracted + assertNotNull(vcpEvent, "VCP event should be extracted"); + assertNotNull(vcpAttachment, "VCP attachment should be extracted"); + assertNotNull(issEvent, "ISS event should be extracted"); + assertNotNull(issAttachment, "ISS attachment should be extracted"); + assertNotNull(acdcEvent, "ACDC event should be extracted"); + + System.out.println("Successfully extracted all credential components"); + return credId; + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Map holderRegistry = testSteps.step("Holder create registry for LE credential", () -> { + String registryName = "vLEI-test-registry-le"; + CreateRegistryArgs registryArgs = CreateRegistryArgs.builder().build(); + registryArgs.setName(holderAid.name); + registryArgs.setRegistryName(registryName); + + try { + RegistryResult regResult = holderClient.registries().create(registryArgs); + waitOperation(holderClient, regResult.op()); + + Object registries = holderClient.registries().list(holderAid.name); + List> registriesList = castObjectToListMap(registries); + + assertTrue(!registriesList.isEmpty()); + return registriesList.getFirst(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + leCredentialId = testSteps.step("Holder create LE (chained) credential and extract components", () -> { + try { + // First, holder must verify the QVI registry using VCP + System.out.println("\n=== Holder Verifying QVI Registry ==="); + + Object op3 = holderClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(holderClient, op3); + + Serder holderVcpSerder = new Serder(vcpEvent); + Object holderRegistryVerifyOp = holderClient.registries().verify(holderVcpSerder, vcpAttachment); + + try { + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + try { + return waitOperation(holderClient, holderRegistryVerifyOp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Operation holderRegistryOperation = future.get(30, TimeUnit.SECONDS); + System.out.println("✓ Holder registry verification completed"); + System.out.println(" Registry Operation Status: " + holderRegistryOperation.isDone()); + } catch (TimeoutException e) { + System.out.println("⚠ Holder registry verification timed out after 30 seconds"); + } catch (Exception e) { + System.out.println("⚠ Holder registry verification failed: " + e.getMessage()); + } + + // Second, holder must verify the QVI credential using ISS and ACDC + System.out.println("\n=== Holder Verifying QVI Credential ==="); + + Object op4 = holderClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(holderClient, op4); + + Serder holderAcdcSerder = new Serder(acdcEvent); + Serder holderIssSerder = new Serder(issEvent); + + Object holderCredentialVerifyOp = holderClient.credentials().verify(holderAcdcSerder, holderIssSerder, issAttachment); + + try { + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + try { + return waitOperation(holderClient, holderCredentialVerifyOp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Operation holderCredentialOperation = future.get(30, TimeUnit.SECONDS); + System.out.println("✓ Holder credential verification completed"); + System.out.println(" Credential Operation Status: " + holderCredentialOperation.isDone()); + } catch (TimeoutException e) { + System.out.println("⚠ Holder credential verification timed out after 30 seconds"); + } catch (Exception e) { + System.out.println("⚠ Holder credential verification failed: " + e.getMessage()); + } + + System.out.println("✓ Holder verification steps completed, now retrieving QVI credential"); + + // Get the QVI credential from holder + Object qviCredential = holderClient.credentials().get(qviCredentialId).get(); + LinkedHashMap qviCredentialBody = castObjectToLinkedHashMap(qviCredential); + LinkedHashMap sadBody = castObjectToLinkedHashMap(qviCredentialBody.get("sad")); + + Map additionalProperties = new LinkedHashMap<>(); + additionalProperties.put("LEI", "5493001KJTIIGC8Y1R17"); + + CredentialData.CredentialSubject cSubject = CredentialData.CredentialSubject.builder().build(); + cSubject.setI(legalEntityAid.prefix); + cSubject.setAdditionalProperties(additionalProperties); + + Map usageDisclaimer = new LinkedHashMap<>(); + usageDisclaimer.put("l", StringData.USAGE_DISCLAIMER); + Map issuanceDisclaimer = new LinkedHashMap<>(); + issuanceDisclaimer.put("l", StringData.ISSUANCE_DISCLAIMER); + + Map sad = new LinkedHashMap<>(); + sad.put("d", ""); + sad.put("usageDisclaimer", usageDisclaimer); + sad.put("issuanceDisclaimer", issuanceDisclaimer); + + Map qvi = new LinkedHashMap<>(); + qvi.put("n", sadBody.get("d")); + qvi.put("s", sadBody.get("s")); + + Map e = new LinkedHashMap<>(); + e.put("d", ""); + e.put("qvi", qvi); + + CredentialData cData = CredentialData.builder().build(); + cData.setA(cSubject); + cData.setRi(holderRegistry.get("regk").toString()); + cData.setS(LE_SCHEMA_SAID); + cData.setR(sad); + cData.setE(e); + + IssueCredentialResult result = holderClient.credentials().issue(holderAid.name, cData); + waitOperation(holderClient, result.getOp()); + String leCredId = result.getAcdc().getKed().get("d").toString(); + + System.out.println("LE Credential Issued Successfully!"); + + // Get the LE credential with CESR format to extract components + Optional leCredentialOpt = holderClient.credentials().get(leCredId, true); + leCredentialCesr = (String) leCredentialOpt.get(); + + // Parse CESR data to extract VCP, ISS, and ACDC events for LE credential + List> leCesrData = parseCESRData(leCredentialCesr); + + // Collect all VCP, ISS, and ACDC events for chained credential verification + List> allVcpEvents = new ArrayList<>(); + List allVcpAttachments = new ArrayList<>(); + List> allIssEvents = new ArrayList<>(); + List allIssAttachments = new ArrayList<>(); + List> allAcdcEvents = new ArrayList<>(); + + for (Map eventData : leCesrData) { + Map event = (Map) eventData.get("event"); + + // Check for event type + Object eventTypeObj = event.get("t"); + if (eventTypeObj != null) { + String eventType = eventTypeObj.toString(); + switch (eventType) { + case "vcp": + allVcpEvents.add(event); + allVcpAttachments.add((String) eventData.get("atc")); + break; + case "iss": + allIssEvents.add(event); + allIssAttachments.add((String) eventData.get("atc")); + break; + } + } else { + // Check if this is an ACDC (credential data) without "t" field + if (event.containsKey("s") && event.containsKey("a") && event.containsKey("i")) { + Object schemaObj = event.get("s"); + if (schemaObj != null) { + allAcdcEvents.add(event); + } + } + } + } + + // Set the LE-specific events (last ones in the chain) + if (!allVcpEvents.isEmpty()) { + leVcpEvent = allVcpEvents.get(allVcpEvents.size() - 1); + leVcpAttachment = allVcpAttachments.get(allVcpAttachments.size() - 1); + } + if (!allIssEvents.isEmpty()) { + leIssEvent = allIssEvents.get(allIssEvents.size() - 1); + leIssAttachment = allIssAttachments.get(allIssAttachments.size() - 1); + } + if (!allAcdcEvents.isEmpty()) { + // Find the LE ACDC event specifically + for (Map acdcEvent : allAcdcEvents) { + Object schemaObj = acdcEvent.get("s"); + if (schemaObj != null && LE_SCHEMA_SAID.equals(schemaObj.toString())) { + leAcdcEvent = acdcEvent; + break; + } + } + } + + // Verify all LE components were extracted + assertNotNull(leVcpEvent, "LE VCP event should be extracted"); + assertNotNull(leVcpAttachment, "LE VCP attachment should be extracted"); + assertNotNull(leIssEvent, "LE ISS event should be extracted"); + assertNotNull(leIssAttachment, "LE ISS attachment should be extracted"); + assertNotNull(leAcdcEvent, "LE ACDC event should be extracted"); + + System.out.println("Successfully extracted all LE credential components"); + return leCredId; + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + testSteps.step("Verifier verify all registries using all VCP events", () -> { + try { + // Query all relevant key states + Object op4 = verifierClient.keyStates().query(holderAid.prefix, "1"); + waitOperation(verifierClient, op4); + Object op5 = verifierClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(verifierClient, op5); + + System.out.println("\n=== Verifying All VCP Events in Chain ==="); + + List> leCesrData = parseCESRData(leCredentialCesr); + + List> allVcpEvents = new ArrayList<>(); + List allVcpAttachments = new ArrayList<>(); + + for (Map eventData : leCesrData) { + Map event = (Map) eventData.get("event"); + Object eventTypeObj = event.get("t"); + if (eventTypeObj != null && "vcp".equals(eventTypeObj.toString())) { + allVcpEvents.add(event); + allVcpAttachments.add((String) eventData.get("atc")); + } + } + + // Verify each VCP event (registry) in the chain + for (int i = 0; i < allVcpEvents.size(); i++) { + Map vcpEvent = allVcpEvents.get(i); + String vcpAttachment = allVcpAttachments.get(i); + Serder vcpSerder = new Serder(vcpEvent); + Object registryVerifyOp = verifierClient.registries().verify(vcpSerder, vcpAttachment); + + try { + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + try { + return waitOperation(verifierClient, registryVerifyOp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Operation registryOperation = future.get(30, TimeUnit.SECONDS); + System.out.println("✓ VCP #" + (i + 1) + " verification completed successfully"); + System.out.println(" Registry Operation Status: " + registryOperation.isDone()); + } catch (TimeoutException e) { + System.out.println("⚠ VCP #" + (i + 1) + " verification timed out after 30 seconds"); + } catch (Exception e) { + System.out.println("⚠ VCP #" + (i + 1) + " verification failed: " + e.getMessage()); + } + } + + System.out.println("Completed verification of " + allVcpEvents.size() + " VCP events in the chain"); + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + testSteps.step("Verifier verify all credentials using all ISS and ACDC events", () -> { + try { + // Query all relevant key states + Object op6 = verifierClient.keyStates().query(holderAid.prefix, "1"); + waitOperation(verifierClient, op6); + Object op7 = verifierClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(verifierClient, op7); + + System.out.println("\n=== Verifying All ISS and ACDC Events in Chain ==="); + + // Parse the existing CESR data to extract ISS and ACDC events + List> leCesrData = parseCESRData(leCredentialCesr); + + List> allIssEvents = new ArrayList<>(); + List allIssAttachments = new ArrayList<>(); + List> allAcdcEvents = new ArrayList<>(); + + // Collect all ISS and ACDC events from the parsed CESR data + for (Map eventData : leCesrData) { + Map event = (Map) eventData.get("event"); + Object eventTypeObj = event.get("t"); + + if (eventTypeObj != null && "iss".equals(eventTypeObj.toString())) { + allIssEvents.add(event); + allIssAttachments.add((String) eventData.get("atc")); + } else if (eventTypeObj == null && event.containsKey("s") && event.containsKey("a") && event.containsKey("i")) { + // This is an ACDC event + allAcdcEvents.add(event); + } + } + + System.out.println("Found " + allIssEvents.size() + " ISS events and " + allAcdcEvents.size() + " ACDC events"); + + // Verify each credential in the chain (ISS + ACDC pairs) + for (int i = 0; i < Math.min(allIssEvents.size(), allAcdcEvents.size()); i++) { + Map issEvent = allIssEvents.get(i); + Map acdcEvent = allAcdcEvents.get(i); + String issAttachment = allIssAttachments.get(i); + + String credentialType = QVI_SCHEMA_SAID.equals(acdcEvent.get("s")) ? "QVI" : + LE_SCHEMA_SAID.equals(acdcEvent.get("s")) ? "LE" : "Unknown"; + Serder acdcSerder = new Serder(acdcEvent); + Serder issSerder = new Serder(issEvent); + + Object credentialVerifyOp = verifierClient.credentials().verify(acdcSerder, issSerder, issAttachment); + + try { + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + try { + return waitOperation(verifierClient, credentialVerifyOp); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + Operation credentialOperation = future.get(30, TimeUnit.SECONDS); + System.out.println("✓ " + credentialType + " credential #" + (i + 1) + " verification completed successfully"); + System.out.println(" Credential Operation Status: " + credentialOperation.isDone()); + } catch (TimeoutException e) { + System.out.println("⚠ " + credentialType + " credential #" + (i + 1) + " verification timed out after 30 seconds"); + } catch (Exception e) { + System.out.println("⚠ " + credentialType + " credential #" + (i + 1) + " verification failed: " + e.getMessage()); + } + } + + // Verify the credential is available from verifier + Optional verifiedLeCredential = verifierClient.credentials().get(leCredentialId, false); + assertTrue(verifiedLeCredential.isPresent(), "Verified LE credential should be retrievable"); + System.out.println("✓ All credentials in the chain verified successfully"); + + // Check for chain information + System.out.println("LE Credential CESR contains QVI reference: " + leCredentialCesr.contains(qviCredentialId)); + + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + static class StringData { + static final String USAGE_DISCLAIMER = "Usage of a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, does not assert that the Legal Entity is trustworthy, honest, reputable in its business dealings, safe to do business with, or compliant with any laws or that an implied or expressly intended purpose will be fulfilled."; + static final String ISSUANCE_DISCLAIMER = "All information in a valid, unexpired, and non-revoked vLEI Credential, as defined in the associated Ecosystem Governance Framework, is accurate as of the date the validation process was complete. The vLEI Credential has been issued to the legal entity or person named in the vLEI Credential as the subject; and the qualified vLEI Issuer exercised reasonable care to perform the validation process set forth in the vLEI Ecosystem Governance Framework."; + } + + /** + * Parses CESR format string into an array of events with their attachments + * CESR format: {json_event}{attachment}{json_event}{attachment}... + * + * @param cesrData The CESR format string + * @return List of maps containing "event" and "atc" keys + */ + @SuppressWarnings("unchecked") + public static List> parseCESRData(String cesrData) { + List> result = new ArrayList<>(); + + int index = 0; + while (index < cesrData.length()) { + // Find the start of JSON event (look for opening brace) + if (cesrData.charAt(index) == '{') { + // Find the end of JSON event by counting braces + int braceCount = 0; + int jsonStart = index; + int jsonEnd = index; + + for (int i = index; i < cesrData.length(); i++) { + char ch = cesrData.charAt(i); + if (ch == '{') { + braceCount++; + } else if (ch == '}') { + braceCount--; + if (braceCount == 0) { + jsonEnd = i + 1; + break; + } + } + } + + // Extract JSON event + String jsonEvent = cesrData.substring(jsonStart, jsonEnd); + + // Find attachment data (everything until next '{' or end of string) + int attachmentStart = jsonEnd; + int attachmentEnd = cesrData.length(); + + for (int i = attachmentStart; i < cesrData.length(); i++) { + if (cesrData.charAt(i) == '{') { + attachmentEnd = i; + break; + } + } + + String attachment = ""; + if (attachmentStart < attachmentEnd) { + attachment = cesrData.substring(attachmentStart, attachmentEnd); + } + + // Parse JSON event to Object + try { + Map eventObj = Utils.fromJson(jsonEvent, Map.class); + + Map eventMap = new LinkedHashMap<>(); + eventMap.put("event", eventObj); + eventMap.put("atc", attachment); + result.add(eventMap); + } catch (Exception e) { + System.err.println("Failed to parse JSON event: " + jsonEvent); + e.printStackTrace(); + } + + index = attachmentEnd; + } else { + index++; + } + } + + return result; + } +} \ No newline at end of file From 7c96b199e355634866ff76bccfb2281483499958 Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Mon, 20 Oct 2025 16:54:55 +0700 Subject: [PATCH 2/2] resolve review comments --- .../credentials/Credentials.java | 14 +- .../signify/e2e/VerifyCredentialTest.java | 346 +++++++----------- 2 files changed, 133 insertions(+), 227 deletions(-) diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java index 0208192..08cf7ee 100644 --- a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java @@ -264,18 +264,24 @@ public RevokeCredentialResult revoke(String name, String said, String datetime) * * @param acdc ACDC to process and verify * @param iss Issuing event for ACDC in TEL - * @param atc Optional attachment string to be verified against the credential + * @param acdcAtc Optional attachment string to be verified against the credential + * @param issAtc Optional attachment string to be verified against the issuing event * @return Operation containing the verification result */ - public Operation verify(Serder acdc, Serder iss, String atc) throws IOException, InterruptedException, LibsodiumException { + public Operation verify(Serder acdc, Serder iss, String acdcAtc, String issAtc) throws IOException, InterruptedException, LibsodiumException { final String path = "/credentials/verify"; final String method = "POST"; Map body = new LinkedHashMap<>(); body.put("acdc", acdc.getKed()); body.put("iss", iss.getKed()); - if (atc != null && !atc.isEmpty()) { - body.put("atc", atc); + + if (acdcAtc != null && !acdcAtc.isEmpty()) { + body.put("acdcAtc", acdcAtc); + } + + if (issAtc != null && !issAtc.isEmpty()) { + body.put("issAtc", issAtc); } HttpResponse response = this.client.fetch(path, method, body); diff --git a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java index 3760132..5a0ded2 100644 --- a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java +++ b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java @@ -14,14 +14,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.security.DigestException; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - +import java.util.concurrent.Callable; import static org.cardanofoundation.signify.e2e.utils.TestUtils.*; import static org.junit.jupiter.api.Assertions.*; @@ -35,7 +29,7 @@ public class VerifyCredentialTest extends BaseIntegrationTest { TestSteps testSteps = new TestSteps(); private static SignifyClient issuerClient, verifierClient, holderClient, legalEntityClient; - private TestUtils.Aid issuerAid, verifierAid, holderAid, legalEntityAid; + private TestUtils.Aid issuerAid, holderAid, legalEntityAid; // Global variables to store QVI credential components private static Map vcpEvent; @@ -72,7 +66,6 @@ public void getAid() throws Exception { new CreateAidArgs(legalEntityClient, "legal-entity") ); issuerAid = aids.get(0); - verifierAid = aids.get(1); holderAid = aids.get(2); legalEntityAid = aids.get(3); } @@ -80,13 +73,11 @@ public void getAid() throws Exception { @BeforeEach public void getContact() { getOrCreateContactAsync( - new GetOrCreateContactArgs(issuerClient, "verifier", verifierAid.oobi), new GetOrCreateContactArgs(issuerClient, "holder", holderAid.oobi), new GetOrCreateContactArgs(verifierClient, "issuer", issuerAid.oobi), new GetOrCreateContactArgs(verifierClient, "holder", holderAid.oobi), new GetOrCreateContactArgs(holderClient, "issuer", issuerAid.oobi), new GetOrCreateContactArgs(holderClient, "legal-entity", legalEntityAid.oobi), - new GetOrCreateContactArgs(holderClient, "verifier", verifierAid.oobi), new GetOrCreateContactArgs(legalEntityClient, "holder", holderAid.oobi), new GetOrCreateContactArgs(legalEntityClient, "issuer", issuerAid.oobi) ); @@ -128,23 +119,20 @@ public void verify_credential_workflow() throws Exception { CreateRegistryArgs registryArgs = CreateRegistryArgs.builder().build(); registryArgs.setName(issuerAid.name); registryArgs.setRegistryName(registryName); - try { - RegistryResult regResult = issuerClient.registries().create(registryArgs); - waitOperation(issuerClient, regResult.op()); - - Object registries = issuerClient.registries().list(issuerAid.name); - List> registriesList = castObjectToListMap(registries); - - registryData.put("name", registriesList.getFirst().get("name").toString()); - registryData.put("regk", registriesList.getFirst().get("regk").toString()); - - assertEquals(1, registriesList.size()); - assertEquals(registryName, registryData.get("name")); - - return registryData; - } catch (IOException | InterruptedException | DigestException e) { - throw new RuntimeException(e); - } + + RegistryResult regResult = issuerClient.registries().create(registryArgs); + waitOperation(issuerClient, regResult.op()); + + Object registries = issuerClient.registries().list(issuerAid.name); + List> registriesList = castObjectToListMap(registries); + + registryData.put("name", registriesList.getFirst().get("name").toString()); + registryData.put("regk", registriesList.getFirst().get("regk").toString()); + + assertEquals(1, registriesList.size()); + assertEquals(registryName, registryData.get("name")); + + return registryData; }); qviCredentialId = testSteps.step("Issue QVI credential and extract components", () -> { @@ -160,60 +148,54 @@ public void verify_credential_workflow() throws Exception { cData.setS(QVI_SCHEMA_SAID); cData.setA(a); - try { - IssueCredentialResult issResult = issuerClient.credentials().issue(issuerAid.name, cData); - waitOperation(issuerClient, issResult.getOp()); - String credId = issResult.getAcdc().getKed().get("d").toString(); - - // Get the credential with CESR format to extract components - Optional credentialOpt = issuerClient.credentials().get(credId, true); - String credentialCesr = (String) credentialOpt.get(); - - // Parse CESR data to extract VCP, ISS, and ACDC events - List> cesrData = parseCESRData(credentialCesr); - - for (Map eventData : cesrData) { - Map event = (Map) eventData.get("event"); - - // Check for event type - Object eventTypeObj = event.get("t"); - if (eventTypeObj != null) { - String eventType = eventTypeObj.toString(); - switch (eventType) { - case "vcp": - vcpEvent = event; - vcpAttachment = (String) eventData.get("atc"); - break; - case "iss": - issEvent = event; - issAttachment = (String) eventData.get("atc"); - break; - } - } else { - // Check if this is an ACDC (credential data) without "t" field - if (event.containsKey("s") && event.containsKey("a") && event.containsKey("i")) { - Object schemaObj = event.get("s"); - if (schemaObj != null && QVI_SCHEMA_SAID.equals(schemaObj.toString())) { - acdcEvent = event; - // System.out.println("Extracted ACDC event"); - } + IssueCredentialResult issResult = issuerClient.credentials().issue(issuerAid.name, cData); + waitOperation(issuerClient, issResult.getOp()); + String credId = issResult.getAcdc().getKed().get("d").toString(); + + // Get the credential with CESR format to extract components + Optional credentialOpt = issuerClient.credentials().get(credId, true); + String credentialCesr = (String) credentialOpt.get(); + + // Parse CESR data to extract VCP, ISS, and ACDC events + List> cesrData = parseCESRData(credentialCesr); + + for (Map eventData : cesrData) { + Map event = (Map) eventData.get("event"); + + // Check for event type + Object eventTypeObj = event.get("t"); + if (eventTypeObj != null) { + String eventType = eventTypeObj.toString(); + switch (eventType) { + case "vcp": + vcpEvent = event; + vcpAttachment = (String) eventData.get("atc"); + break; + case "iss": + issEvent = event; + issAttachment = (String) eventData.get("atc"); + break; + } + } else { + // Check if this is an ACDC (credential data) without "t" field + if (event.containsKey("s") && event.containsKey("a") && event.containsKey("i")) { + Object schemaObj = event.get("s"); + if (schemaObj != null && QVI_SCHEMA_SAID.equals(schemaObj.toString())) { + acdcEvent = event; } } } - - // Verify all components were extracted - assertNotNull(vcpEvent, "VCP event should be extracted"); - assertNotNull(vcpAttachment, "VCP attachment should be extracted"); - assertNotNull(issEvent, "ISS event should be extracted"); - assertNotNull(issAttachment, "ISS attachment should be extracted"); - assertNotNull(acdcEvent, "ACDC event should be extracted"); - - System.out.println("Successfully extracted all credential components"); - return credId; - - } catch (Exception e) { - throw new RuntimeException(e); } + + // Verify all components were extracted + assertNotNull(vcpEvent, "VCP event should be extracted"); + assertNotNull(vcpAttachment, "VCP attachment should be extracted"); + assertNotNull(issEvent, "ISS event should be extracted"); + assertNotNull(issAttachment, "ISS attachment should be extracted"); + assertNotNull(acdcEvent, "ACDC event should be extracted"); + + System.out.println("Successfully extracted all credential components"); + return credId; }); Map holderRegistry = testSteps.step("Holder create registry for LE credential", () -> { @@ -222,23 +204,18 @@ public void verify_credential_workflow() throws Exception { registryArgs.setName(holderAid.name); registryArgs.setRegistryName(registryName); - try { - RegistryResult regResult = holderClient.registries().create(registryArgs); - waitOperation(holderClient, regResult.op()); - - Object registries = holderClient.registries().list(holderAid.name); - List> registriesList = castObjectToListMap(registries); - - assertTrue(!registriesList.isEmpty()); - return registriesList.getFirst(); - } catch (Exception e) { - throw new RuntimeException(e); - } + RegistryResult regResult = holderClient.registries().create(registryArgs); + waitOperation(holderClient, regResult.op()); + + Object registries = holderClient.registries().list(holderAid.name); + List> registriesList = castObjectToListMap(registries); + + assertTrue(!registriesList.isEmpty()); + return registriesList.getFirst(); }); leCredentialId = testSteps.step("Holder create LE (chained) credential and extract components", () -> { - try { - // First, holder must verify the QVI registry using VCP + // First, holder must verify the QVI registry using VCP System.out.println("\n=== Holder Verifying QVI Registry ==="); Object op3 = holderClient.keyStates().query(issuerAid.prefix, "1"); @@ -247,23 +224,8 @@ public void verify_credential_workflow() throws Exception { Serder holderVcpSerder = new Serder(vcpEvent); Object holderRegistryVerifyOp = holderClient.registries().verify(holderVcpSerder, vcpAttachment); - try { - CompletableFuture> future = CompletableFuture.supplyAsync(() -> { - try { - return waitOperation(holderClient, holderRegistryVerifyOp); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - - Operation holderRegistryOperation = future.get(30, TimeUnit.SECONDS); - System.out.println("✓ Holder registry verification completed"); - System.out.println(" Registry Operation Status: " + holderRegistryOperation.isDone()); - } catch (TimeoutException e) { - System.out.println("⚠ Holder registry verification timed out after 30 seconds"); - } catch (Exception e) { - System.out.println("⚠ Holder registry verification failed: " + e.getMessage()); - } + Operation holderRegistryOperation = waitOperation(holderClient, holderRegistryVerifyOp); + assertTrue(holderRegistryOperation.isDone()); // Second, holder must verify the QVI credential using ISS and ACDC System.out.println("\n=== Holder Verifying QVI Credential ==="); @@ -273,26 +235,11 @@ public void verify_credential_workflow() throws Exception { Serder holderAcdcSerder = new Serder(acdcEvent); Serder holderIssSerder = new Serder(issEvent); - - Object holderCredentialVerifyOp = holderClient.credentials().verify(holderAcdcSerder, holderIssSerder, issAttachment); - - try { - CompletableFuture> future = CompletableFuture.supplyAsync(() -> { - try { - return waitOperation(holderClient, holderCredentialVerifyOp); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - - Operation holderCredentialOperation = future.get(30, TimeUnit.SECONDS); - System.out.println("✓ Holder credential verification completed"); - System.out.println(" Credential Operation Status: " + holderCredentialOperation.isDone()); - } catch (TimeoutException e) { - System.out.println("⚠ Holder credential verification timed out after 30 seconds"); - } catch (Exception e) { - System.out.println("⚠ Holder credential verification failed: " + e.getMessage()); - } + + Object holderCredentialVerifyOp = holderClient.credentials().verify(holderAcdcSerder, holderIssSerder, null, issAttachment); + + Operation holderCredentialOperation = waitOperation(holderClient, holderCredentialVerifyOp); + assertTrue(holderCredentialOperation.isDone()); System.out.println("✓ Holder verification steps completed, now retrieving QVI credential"); @@ -410,78 +357,55 @@ public void verify_credential_workflow() throws Exception { System.out.println("Successfully extracted all LE credential components"); return leCredId; - - } catch (Exception e) { - throw new RuntimeException(e); - } }); - testSteps.step("Verifier verify all registries using all VCP events", () -> { - try { - // Query all relevant key states - Object op4 = verifierClient.keyStates().query(holderAid.prefix, "1"); - waitOperation(verifierClient, op4); - Object op5 = verifierClient.keyStates().query(issuerAid.prefix, "1"); - waitOperation(verifierClient, op5); - - System.out.println("\n=== Verifying All VCP Events in Chain ==="); - - List> leCesrData = parseCESRData(leCredentialCesr); - - List> allVcpEvents = new ArrayList<>(); - List allVcpAttachments = new ArrayList<>(); - - for (Map eventData : leCesrData) { - Map event = (Map) eventData.get("event"); - Object eventTypeObj = event.get("t"); - if (eventTypeObj != null && "vcp".equals(eventTypeObj.toString())) { - allVcpEvents.add(event); - allVcpAttachments.add((String) eventData.get("atc")); - } - } - - // Verify each VCP event (registry) in the chain - for (int i = 0; i < allVcpEvents.size(); i++) { - Map vcpEvent = allVcpEvents.get(i); - String vcpAttachment = allVcpAttachments.get(i); - Serder vcpSerder = new Serder(vcpEvent); - Object registryVerifyOp = verifierClient.registries().verify(vcpSerder, vcpAttachment); - - try { - CompletableFuture> future = CompletableFuture.supplyAsync(() -> { - try { - return waitOperation(verifierClient, registryVerifyOp); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - - Operation registryOperation = future.get(30, TimeUnit.SECONDS); - System.out.println("✓ VCP #" + (i + 1) + " verification completed successfully"); - System.out.println(" Registry Operation Status: " + registryOperation.isDone()); - } catch (TimeoutException e) { - System.out.println("⚠ VCP #" + (i + 1) + " verification timed out after 30 seconds"); - } catch (Exception e) { - System.out.println("⚠ VCP #" + (i + 1) + " verification failed: " + e.getMessage()); - } + testSteps.steps("Verifier verify all registries using all VCP events", (Callable) () -> { + // Query all relevant key states + Object op4 = verifierClient.keyStates().query(holderAid.prefix, "1"); + waitOperation(verifierClient, op4); + Object op5 = verifierClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(verifierClient, op5); + + System.out.println("\n=== Verifying All VCP Events in Chain ==="); + + List> leCesrData = parseCESRData(leCredentialCesr); + + List> allVcpEvents = new ArrayList<>(); + List allVcpAttachments = new ArrayList<>(); + + for (Map eventData : leCesrData) { + Map event = (Map) eventData.get("event"); + Object eventTypeObj = event.get("t"); + if (eventTypeObj != null && "vcp".equals(eventTypeObj.toString())) { + allVcpEvents.add(event); + allVcpAttachments.add((String) eventData.get("atc")); } - - System.out.println("Completed verification of " + allVcpEvents.size() + " VCP events in the chain"); - - } catch (Exception e) { - throw new RuntimeException(e); } + + // Verify each VCP event (registry) in the chain + for (int i = 0; i < allVcpEvents.size(); i++) { + Map vcpEvent = allVcpEvents.get(i); + String vcpAttachment = allVcpAttachments.get(i); + Serder vcpSerder = new Serder(vcpEvent); + Object registryVerifyOp = verifierClient.registries().verify(vcpSerder, vcpAttachment); + + Operation registryOperation = waitOperation(verifierClient, registryVerifyOp); + assertTrue(registryOperation.isDone()); + System.out.println("✓ VCP #" + (i + 1) + " verification completed successfully"); + } + + System.out.println("Completed verification of " + allVcpEvents.size() + " VCP events in the chain"); + return null; }); - testSteps.step("Verifier verify all credentials using all ISS and ACDC events", () -> { - try { - // Query all relevant key states - Object op6 = verifierClient.keyStates().query(holderAid.prefix, "1"); - waitOperation(verifierClient, op6); - Object op7 = verifierClient.keyStates().query(issuerAid.prefix, "1"); - waitOperation(verifierClient, op7); + testSteps.steps("Verifier verify all credentials using all ISS and ACDC events", (Callable) () -> { + // Query all relevant key states + Object op6 = verifierClient.keyStates().query(holderAid.prefix, "1"); + waitOperation(verifierClient, op6); + Object op7 = verifierClient.keyStates().query(issuerAid.prefix, "1"); + waitOperation(verifierClient, op7); - System.out.println("\n=== Verifying All ISS and ACDC Events in Chain ==="); + System.out.println("\n=== Verifying All ISS and ACDC Events in Chain ==="); // Parse the existing CESR data to extract ISS and ACDC events List> leCesrData = parseCESRData(leCredentialCesr); @@ -504,38 +428,17 @@ public void verify_credential_workflow() throws Exception { } } - System.out.println("Found " + allIssEvents.size() + " ISS events and " + allAcdcEvents.size() + " ACDC events"); - // Verify each credential in the chain (ISS + ACDC pairs) for (int i = 0; i < Math.min(allIssEvents.size(), allAcdcEvents.size()); i++) { Map issEvent = allIssEvents.get(i); Map acdcEvent = allAcdcEvents.get(i); String issAttachment = allIssAttachments.get(i); - - String credentialType = QVI_SCHEMA_SAID.equals(acdcEvent.get("s")) ? "QVI" : - LE_SCHEMA_SAID.equals(acdcEvent.get("s")) ? "LE" : "Unknown"; Serder acdcSerder = new Serder(acdcEvent); Serder issSerder = new Serder(issEvent); - Object credentialVerifyOp = verifierClient.credentials().verify(acdcSerder, issSerder, issAttachment); - - try { - CompletableFuture> future = CompletableFuture.supplyAsync(() -> { - try { - return waitOperation(verifierClient, credentialVerifyOp); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - - Operation credentialOperation = future.get(30, TimeUnit.SECONDS); - System.out.println("✓ " + credentialType + " credential #" + (i + 1) + " verification completed successfully"); - System.out.println(" Credential Operation Status: " + credentialOperation.isDone()); - } catch (TimeoutException e) { - System.out.println("⚠ " + credentialType + " credential #" + (i + 1) + " verification timed out after 30 seconds"); - } catch (Exception e) { - System.out.println("⚠ " + credentialType + " credential #" + (i + 1) + " verification failed: " + e.getMessage()); - } + Object credentialVerifyOp = verifierClient.credentials().verify(acdcSerder, issSerder, null, issAttachment); + Operation credentialOperation = waitOperation(verifierClient, credentialVerifyOp); + assertTrue(credentialOperation.isDone()); } // Verify the credential is available from verifier @@ -544,11 +447,8 @@ public void verify_credential_workflow() throws Exception { System.out.println("✓ All credentials in the chain verified successfully"); // Check for chain information - System.out.println("LE Credential CESR contains QVI reference: " + leCredentialCesr.contains(qviCredentialId)); - - } catch (Exception e) { - throw new RuntimeException(e); - } + assertTrue(leCredentialCesr.contains(qviCredentialId)); + return null; }); }