From af01d93aefdb3c99f81537af2eecdc7ee5aef15f Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Thu, 9 Oct 2025 14:45:56 +0700 Subject: [PATCH 1/7] 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 bc434d1e..02081920 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 41626b5d..3ef58394 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 00000000..3760132a --- /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/7] 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 02081920..08cf7ee7 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 3760132a..5a0ded22 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; }); } From 2c2e387855a2ea21c85486a0dcab31327e0d454d Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Wed, 22 Oct 2025 15:40:17 +0700 Subject: [PATCH 3/7] fix and update new KERIA/Witness docker images This reverts commit b4be39111d1f7e8359ad757cccb25b152af5c12f. --- docker-compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 114ea49c..2267d47c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,13 +21,13 @@ services: - 7723:7723 keria: - image: ${KERIA_IMAGE:-weboftrust/keria}:${KERIA_IMAGE_TAG:-0.2.0-dev6} + image: cardanofoundation/cf-idw-keria:c6498308 environment: KERI_AGENT_CORS: 1 <<: *python-env volumes: - ./config/keria.json:/keria/config/keri/cf/keria.json - command: start --config-dir /keria/config --config-file keria.json --name agent # Adjusted command line + entrypoint: keria start --config-dir /keria/config --config-file keria.json --name agent # Adjusted command line healthcheck: test: wget --spider http://keria:3902/spec.yaml <<: *healthcheck @@ -48,4 +48,4 @@ services: ports: - 5642:5642 - 5643:5643 - - 5644:5644 + - 5644:5644 \ No newline at end of file From 072287911294e662354ffd8a708b9664a8efacdb Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Mon, 3 Nov 2025 15:43:56 +0700 Subject: [PATCH 4/7] resolve review comments --- .../credentials/CredentialVerifyOptions.java | 51 ++ .../credentials/Credentials.java | 76 +-- .../credentialing/registries/Registries.java | 15 +- .../registries/RegistryVerifyOptions.java | 35 ++ .../signify/app/BaseMockServerTest.java | 16 + .../signify/app/CredentialingTest.java | 11 +- .../signify/e2e/CredentialsTest.java | 18 +- .../signify/e2e/VerifyCredentialTest.java | 467 +++++++++--------- .../signify/e2e/utils/TestUtils.java | 2 +- 9 files changed, 418 insertions(+), 273 deletions(-) create mode 100644 src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/CredentialVerifyOptions.java create mode 100644 src/main/java/org/cardanofoundation/signify/app/credentialing/registries/RegistryVerifyOptions.java diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/CredentialVerifyOptions.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/CredentialVerifyOptions.java new file mode 100644 index 00000000..b862b2bc --- /dev/null +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/CredentialVerifyOptions.java @@ -0,0 +1,51 @@ +package org.cardanofoundation.signify.app.credentialing.credentials; + +import org.cardanofoundation.signify.cesr.Serder; + +public class CredentialVerifyOptions { + private final Serder acdc; + private final Serder iss; + private final String acdcAtc; + private final String issAtc; + + private CredentialVerifyOptions(Builder builder) { + this.acdc = builder.acdc; + this.iss = builder.iss; + this.acdcAtc = builder.acdcAtc; + this.issAtc = builder.issAtc; + } + + public Serder getAcdc() { return acdc; } + public Serder getIss() { return iss; } + public String getAcdcAtc() { return acdcAtc; } + public String getIssAtc() { return issAtc; } + + public static Builder builder() { return new Builder(); } + + public static class Builder { + private Serder acdc; + private Serder iss; + private String acdcAtc; + private String issAtc; + + public Builder acdc(Serder acdc) { + this.acdc = acdc; + return this; + } + public Builder iss(Serder iss) { + this.iss = iss; + return this; + } + public Builder acdcAtc(String acdcAtc) { + this.acdcAtc = acdcAtc; + return this; + } + public Builder issAtc(String issAtc) { + this.issAtc = issAtc; + return this; + } + public CredentialVerifyOptions build() { + return new CredentialVerifyOptions(this); + } + } +} \ No newline at end of file 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 08cf7ee7..0de5b110 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 @@ -47,37 +47,58 @@ public Object list(CredentialFilter kargs) throws IOException, InterruptedExcept return Utils.fromJson(response.body(), Object.class); } - public Optional get(String said) throws IOException, InterruptedException, LibsodiumException { - return this.get(said, false); + /** + * Get a credential as raw CESR string. + */ + public Optional get(String said) throws IOException, InterruptedException, LibsodiumException { + return this.getCESR(said); } /** - * Get a credential - * - * @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. - * Returns String (raw CESR text) when includeCESR=true, - * or Object (parsed JSON) when includeCESR=false + * Get a credential as parsed JSON (Object) or raw CESR string depending on includeCESR. */ public Optional get(String said, boolean includeCESR) throws IOException, InterruptedException, LibsodiumException { + if (includeCESR) { + // For backward compatibility, but prefer getCESR for type safety + Optional cesr = getCESR(said); + return cesr.map(s -> (Object) s); + } else { + return getJson(said); + } + } + + /** + * Get a credential as raw CESR string. + */ + public Optional getCESR(String said) throws IOException, InterruptedException, LibsodiumException { final String path = "/credentials/" + said; final String method = "GET"; - Map extraHeaders = new LinkedHashMap<>(); - if (includeCESR) { - extraHeaders.put("Accept", "application/json+cesr"); - } else { - extraHeaders.put("Accept", "application/json"); + extraHeaders.put("Accept", "application/json+cesr"); + + HttpResponse response = this.client.fetch(path, method, null, extraHeaders); + + if (response.statusCode() == java.net.HttpURLConnection.HTTP_NOT_FOUND) { + return Optional.empty(); } + return Optional.of(response.body()); + } + + /** + * Get a credential as parsed JSON (Object). + */ + private Optional getJson(String said) throws IOException, InterruptedException, LibsodiumException { + final String path = "/credentials/" + said; + final String method = "GET"; + Map extraHeaders = new LinkedHashMap<>(); + extraHeaders.put("Accept", "application/json"); HttpResponse response = this.client.fetch(path, method, null, extraHeaders); - + if (response.statusCode() == java.net.HttpURLConnection.HTTP_NOT_FOUND) { return Optional.empty(); } - - return Optional.of(includeCESR ? response.body() : Utils.fromJson(response.body(), Object.class)); + return Optional.of(Utils.fromJson(response.body(), Object.class)); } /** @@ -198,7 +219,7 @@ public RevokeCredentialResult revoke(String name, String said, String datetime) final String vs = CoreUtil.versify(CoreUtil.Ident.KERI, null, CoreUtil.Serials.JSON, 0); final String dt = datetime != null ? datetime : Utils.currentDateTimeString(); - Map cred = Utils.toMap(this.get(said) + Map cred = Utils.toMap(this.get(said, false) .orElseThrow(() -> new IllegalArgumentException("Credential not found: " + said))); // Create rev @@ -262,26 +283,23 @@ public RevokeCredentialResult revoke(String name, String said, String datetime) /** * Verify a credential and issuing event * - * @param acdc ACDC to process and verify - * @param iss Issuing event for ACDC in TEL - * @param acdcAtc Optional attachment string to be verified against the credential - * @param issAtc Optional attachment string to be verified against the issuing event + * @param options CredentialVerifyOptions containing all verification parameters * @return Operation containing the verification result */ - public Operation verify(Serder acdc, Serder iss, String acdcAtc, String issAtc) throws IOException, InterruptedException, LibsodiumException { + public Operation verify(CredentialVerifyOptions options) 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()); + body.put("acdc", options.getAcdc().getKed()); + body.put("iss", options.getIss().getKed()); - if (acdcAtc != null && !acdcAtc.isEmpty()) { - body.put("acdcAtc", acdcAtc); + if (options.getAcdcAtc() != null && !options.getAcdcAtc().isEmpty()) { + body.put("acdcAtc", options.getAcdcAtc()); } - if (issAtc != null && !issAtc.isEmpty()) { - body.put("issAtc", issAtc); + if (options.getIssAtc() != null && !options.getIssAtc().isEmpty()) { + body.put("issAtc", options.getIssAtc()); } HttpResponse response = this.client.fetch(path, method, body); 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 3ef58394..3e6378c2 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 @@ -170,23 +170,22 @@ public Object rename(String name, String registryName, String newName) throws IO /** * Verify a registry with optional attachment * - * @param vcp the VCP (Verifiable Credential Protocol) data to verify - * @param atc the optional attachment data (metadata) + * @param options RegistryVerifyOptions containing all verification parameters * @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 { + public Operation verify(RegistryVerifyOptions options) 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); + body.put("vcp", options.getVcp().getKed()); + if (options.getAtc() != null && !options.getAtc().isEmpty()) { + body.put("atc", options.getAtc()); } - + 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/RegistryVerifyOptions.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/RegistryVerifyOptions.java new file mode 100644 index 00000000..dfdb3601 --- /dev/null +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/RegistryVerifyOptions.java @@ -0,0 +1,35 @@ +package org.cardanofoundation.signify.app.credentialing.registries; + +import org.cardanofoundation.signify.cesr.Serder; + +public class RegistryVerifyOptions { + private final Serder vcp; + private final String atc; + + private RegistryVerifyOptions(Builder builder) { + this.vcp = builder.vcp; + this.atc = builder.atc; + } + + public Serder getVcp() { return vcp; } + public String getAtc() { return atc; } + + public static Builder builder() { return new Builder(); } + + public static class Builder { + private Serder vcp; + private String atc; + + public Builder vcp(Serder vcp) { + this.vcp = vcp; + return this; + } + public Builder atc(String atc) { + this.atc = atc; + return this; + } + public RegistryVerifyOptions build() { + return new RegistryVerifyOptions(this); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/cardanofoundation/signify/app/BaseMockServerTest.java b/src/test/java/org/cardanofoundation/signify/app/BaseMockServerTest.java index 9dac766b..156d4b98 100644 --- a/src/test/java/org/cardanofoundation/signify/app/BaseMockServerTest.java +++ b/src/test/java/org/cardanofoundation/signify/app/BaseMockServerTest.java @@ -188,6 +188,22 @@ void tearDown() throws Exception { "windexes": [] }"""; + public static final String MOCK_REGISTRY_STATE = """ + { + "v": "KERI10JSON000135_", + "i": "EMwcsEMUEruPXVwPCW7zmqmN8m0I3CihxolBm-RDrsJo", + "s": "0", + "d": "ENf3IEYwYtFmlq5ZzoI-zFzeR7E3ZNRN2YH_0KAFbdJW", + "ri": "EGK216v1yguLfex4YRFnG7k1sXRjh3OKY7QqzdKsx7df", + "ra": {}, + "a": { + "s": 2, + "d": "EIpgyKVF0z0Pcn2_HgbWhEKmJhOXFeD4SA62SrxYXOLt" + }, + "dt": "2023-08-23T15:16:07.553000+00:00", + "et": "iss" + }"""; + public static final String MOCK_CREDENTIAL = """ { "sad": { diff --git a/src/test/java/org/cardanofoundation/signify/app/CredentialingTest.java b/src/test/java/org/cardanofoundation/signify/app/CredentialingTest.java index b8635ba3..b49a2943 100644 --- a/src/test/java/org/cardanofoundation/signify/app/CredentialingTest.java +++ b/src/test/java/org/cardanofoundation/signify/app/CredentialingTest.java @@ -47,9 +47,14 @@ public MockResponse mockAllRequests(RecordedRequest req) throws LibsodiumExcepti null ); - String body = reqUrl.startsWith(url + "/credentials") - ? MOCK_CREDENTIAL - : MOCK_GET_AID; + String body; + if (reqUrl.startsWith(url + "/credentials")) { + body = MOCK_CREDENTIAL; + } else if (reqUrl.startsWith(url + "/registries")) { + body = MOCK_REGISTRY_STATE; + } else { + body = MOCK_GET_AID; + } MockResponse mockResponse = new MockResponse() .setResponseCode(202) diff --git a/src/test/java/org/cardanofoundation/signify/e2e/CredentialsTest.java b/src/test/java/org/cardanofoundation/signify/e2e/CredentialsTest.java index 996b8346..0de62e3c 100644 --- a/src/test/java/org/cardanofoundation/signify/e2e/CredentialsTest.java +++ b/src/test/java/org/cardanofoundation/signify/e2e/CredentialsTest.java @@ -246,7 +246,7 @@ public void single_signature_credentials() throws Exception { testSteps.step("Issuer get credential by id", () -> { try { - Object issuerCredential = issuerClient.credentials().get(qviCredentialId).get(); + Object issuerCredential = issuerClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap issuerCredentialsList = castObjectToLinkedHashMap(issuerCredential); Object credentialsMap = issuerCredentialsList.get("sad"); LinkedHashMap sad = castObjectToLinkedHashMap(credentialsMap); @@ -264,7 +264,7 @@ public void single_signature_credentials() throws Exception { testSteps.step("Issuer IPEX grant", () -> { String dt = createTimestamp(); try { - Object issuerCredential = issuerClient.credentials().get(qviCredentialId).get(); + Object issuerCredential = issuerClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap issuerCredentialList = castObjectToLinkedHashMap(issuerCredential); Map getSAD = (Map) issuerCredentialList.get("sad"); Map getANC = (Map) issuerCredentialList.get("anc"); @@ -340,7 +340,7 @@ public void single_signature_credentials() throws Exception { Map sad, status; String atc; try { - Object holderCredential = holderClient.credentials().get(qviCredentialId).get(); + Object holderCredential = holderClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap holderCredentialList = castObjectToLinkedHashMap(holderCredential); Object credentialsMap = holderCredentialList.get("sad"); @@ -490,7 +490,7 @@ public void single_signature_credentials() throws Exception { markAndRemoveNotification(holderClient, holderAgreeNote); - Object holderCredential = holderClient.credentials().get(qviCredentialId).get(); + Object holderCredential = holderClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap holderCredentialBody = castObjectToLinkedHashMap(holderCredential); LinkedHashMap sad = castObjectToLinkedHashMap(holderCredentialBody.get("sad")); LinkedHashMap anc = castObjectToLinkedHashMap(holderCredentialBody.get("anc")); @@ -550,7 +550,7 @@ public void single_signature_credentials() throws Exception { ); waitOperation(verifierClient, op); markAndRemoveNotification(verifierClient, verifierGrantNote); - Object verifierCredential = verifierClient.credentials().get(qviCredentialId).get(); + Object verifierCredential = verifierClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap verifierCredentialBody = castObjectToLinkedHashMap(verifierCredential); LinkedHashMap sad = castObjectToLinkedHashMap(verifierCredentialBody.get("sad")); @@ -599,7 +599,7 @@ public void single_signature_credentials() throws Exception { String leCredentialId = testSteps.step("Holder create LE (chained) credential", () -> { try { - Object qviCredential = holderClient.credentials().get(qviCredentialId).get(); + Object qviCredential = holderClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap qviCredentialBody = castObjectToLinkedHashMap(qviCredential); LinkedHashMap sadBody = castObjectToLinkedHashMap(qviCredentialBody.get("sad")); @@ -646,7 +646,7 @@ public void single_signature_credentials() throws Exception { testSteps.step("LE credential IPEX grant", () -> { String dt = createTimestamp(); try { - Object leCredential = holderClient.credentials().get(leCredentialId).get(); + Object leCredential = holderClient.credentials().get(leCredentialId, false).get(); LinkedHashMap leCredentialBody = castObjectToLinkedHashMap(leCredential); assertTrue(!leCredentialBody.isEmpty()); @@ -710,7 +710,7 @@ public void single_signature_credentials() throws Exception { Object legalEntityCredential = retry(() -> { try { assertNotNull(leCredentialId); - return legalEntityClient.credentials().get(leCredentialId).get(); + return legalEntityClient.credentials().get(leCredentialId, false).get(); } catch (Exception e) { throw new RuntimeException(e); } @@ -736,7 +736,7 @@ public void single_signature_credentials() throws Exception { try { RevokeCredentialResult revokeOperation = issuerClient.credentials().revoke(issuerAid.name, qviCredentialId, null); waitOperation(issuerClient, revokeOperation.getOp()); - Object issuerCredential = issuerClient.credentials().get(qviCredentialId).get(); + Object issuerCredential = issuerClient.credentials().get(qviCredentialId, false).get(); LinkedHashMap issuerCredentialBody = castObjectToLinkedHashMap(issuerCredential); LinkedHashMap status = castObjectToLinkedHashMap(issuerCredentialBody.get("status")); diff --git a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java index 5a0ded22..d0ccafb6 100644 --- a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java +++ b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java @@ -5,6 +5,7 @@ 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.app.credentialing.registries.RegistryVerifyOptions; import org.cardanofoundation.signify.cesr.Serder; import org.cardanofoundation.signify.cesr.util.Utils; import org.cardanofoundation.signify.e2e.utils.ResolveEnv; @@ -30,7 +31,7 @@ public class VerifyCredentialTest extends BaseIntegrationTest { private static SignifyClient issuerClient, verifierClient, holderClient, legalEntityClient; private TestUtils.Aid issuerAid, holderAid, legalEntityAid; - + // Global variables to store QVI credential components private static Map vcpEvent; private static String vcpAttachment; @@ -38,7 +39,7 @@ public class VerifyCredentialTest extends BaseIntegrationTest { 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; @@ -63,8 +64,7 @@ public void getAid() throws Exception { new CreateAidArgs(issuerClient, "issuer"), new CreateAidArgs(verifierClient, "verifier"), new CreateAidArgs(holderClient, "holder"), - new CreateAidArgs(legalEntityClient, "legal-entity") - ); + new CreateAidArgs(legalEntityClient, "legal-entity")); issuerAid = aids.get(0); holderAid = aids.get(2); legalEntityAid = aids.get(3); @@ -79,8 +79,7 @@ public void getContact() { new GetOrCreateContactArgs(holderClient, "issuer", issuerAid.oobi), new GetOrCreateContactArgs(holderClient, "legal-entity", legalEntityAid.oobi), new GetOrCreateContactArgs(legalEntityClient, "holder", holderAid.oobi), - new GetOrCreateContactArgs(legalEntityClient, "issuer", issuerAid.oobi) - ); + new GetOrCreateContactArgs(legalEntityClient, "issuer", issuerAid.oobi)); System.out.println("Created contact successfully"); } @@ -90,8 +89,7 @@ public static void cleanup() throws Exception { issuerClient, verifierClient, holderClient, - legalEntityClient - ); + legalEntityClient); assertOperations(clients); assertNotifications(clients); } @@ -108,8 +106,7 @@ public void verify_credential_workflow() throws Exception { 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) - ); + new ResolveOobisArgs(legalEntityClient, LE_SCHEMA_URL, null)); }); HashMap registry = testSteps.step("Create registry", () -> { @@ -119,19 +116,19 @@ public void verify_credential_workflow() throws Exception { CreateRegistryArgs registryArgs = CreateRegistryArgs.builder().build(); registryArgs.setName(issuerAid.name); registryArgs.setRegistryName(registryName); - + 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; }); @@ -151,17 +148,16 @@ public void verify_credential_workflow() throws Exception { 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(); - + String credentialCesr = issuerClient.credentials().get(credId).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) { @@ -186,14 +182,14 @@ public void verify_credential_workflow() throws Exception { } } } - + // 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; }); @@ -206,157 +202,170 @@ public void verify_credential_workflow() throws Exception { 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", () -> { - // 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); - - 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 ==="); - - 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, null, issAttachment); - - Operation holderCredentialOperation = waitOperation(holderClient, holderCredentialVerifyOp); - assertTrue(holderCredentialOperation.isDone()); - - 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); - } + // 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); + + RegistryVerifyOptions holderRegistryVerifyOptions = RegistryVerifyOptions.builder() + .vcp(holderVcpSerder) + .atc(vcpAttachment) + .build(); + + Object holderRegistryVerifyOp = holderClient.registries().verify(holderRegistryVerifyOptions); + + Operation holderRegistryOperation = waitOperation(holderClient, holderRegistryVerifyOp); + assertTrue(holderRegistryOperation.isDone()); + + // 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, "2"); + waitOperation(holderClient, op4); + + Serder holderAcdcSerder = new Serder(acdcEvent); + Serder holderIssSerder = new Serder(issEvent); + + CredentialVerifyOptions holderVerifyOptions = CredentialVerifyOptions.builder() + .acdc(holderAcdcSerder) + .iss(holderIssSerder) + .issAtc(issAttachment) + .build(); + + Object holderCredentialVerifyOp = holderClient.credentials().verify(holderVerifyOptions); + + Operation holderCredentialOperation = waitOperation(holderClient, holderCredentialVerifyOp); + assertTrue(holderCredentialOperation.isDone()); + + System.out.println("✓ Holder verification steps completed, now retrieving QVI credential"); + + // Get the QVI credential from holder + Object qviCredential = holderClient.credentials().get(qviCredentialId, false).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 leCredentialCesrOpt = holderClient.credentials().get(leCredId); + assertTrue(leCredentialCesrOpt.isPresent(), "LE credential CESR should be present"); + leCredentialCesr = leCredentialCesrOpt.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; - } + } + + // 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; + } + + // 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; }); testSteps.steps("Verifier verify all registries using all VCP events", (Callable) () -> { @@ -365,14 +374,14 @@ public void verify_credential_workflow() throws Exception { 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"); @@ -381,73 +390,85 @@ public void verify_credential_workflow() throws Exception { 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); - + + RegistryVerifyOptions registryVerifyOptions = RegistryVerifyOptions.builder() + .vcp(vcpSerder) + .atc(vcpAttachment) + .build(); + + Object registryVerifyOp = verifierClient.registries().verify(registryVerifyOptions); + 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.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"); + Object op6 = verifierClient.keyStates().query(holderAid.prefix, "2"); waitOperation(verifierClient, op6); - Object op7 = verifierClient.keyStates().query(issuerAid.prefix, "1"); + Object op7 = verifierClient.keyStates().query(issuerAid.prefix, "2"); 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); - } - } - - // 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); - Serder acdcSerder = new Serder(acdcEvent); - Serder issSerder = new Serder(issEvent); - - Object credentialVerifyOp = verifierClient.credentials().verify(acdcSerder, issSerder, null, issAttachment); - Operation credentialOperation = waitOperation(verifierClient, credentialVerifyOp); - assertTrue(credentialOperation.isDone()); + + // 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); } - - // 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 - assertTrue(leCredentialCesr.contains(qviCredentialId)); + } + + // 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); + Serder acdcSerder = new Serder(acdcEvent); + Serder issSerder = new Serder(issEvent); + CredentialVerifyOptions verifyOptions = CredentialVerifyOptions.builder() + .acdc(acdcSerder) + .iss(issSerder) + .issAtc(issAttachment) + .build(); + + Object credentialVerifyOp = verifierClient.credentials().verify(verifyOptions); + Operation credentialOperation = waitOperation(verifierClient, credentialVerifyOp); + assertTrue(credentialOperation.isDone()); + } + + // 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 + assertTrue(leCredentialCesr.contains(qviCredentialId)); return null; }); } @@ -467,7 +488,7 @@ static class StringData { @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) @@ -476,7 +497,7 @@ public static List> parseCESRData(String cesrData) { int braceCount = 0; int jsonStart = index; int jsonEnd = index; - + for (int i = index; i < cesrData.length(); i++) { char ch = cesrData.charAt(i); if (ch == '{') { @@ -489,30 +510,30 @@ public static List> parseCESRData(String cesrData) { } } } - + // 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); @@ -521,13 +542,13 @@ public static List> parseCESRData(String cesrData) { System.err.println("Failed to parse JSON event: " + jsonEvent); e.printStackTrace(); } - + index = attachmentEnd; } else { index++; } } - + return result; } } \ No newline at end of file diff --git a/src/test/java/org/cardanofoundation/signify/e2e/utils/TestUtils.java b/src/test/java/org/cardanofoundation/signify/e2e/utils/TestUtils.java index df3178ca..69476f9f 100644 --- a/src/test/java/org/cardanofoundation/signify/e2e/utils/TestUtils.java +++ b/src/test/java/org/cardanofoundation/signify/e2e/utils/TestUtils.java @@ -362,7 +362,7 @@ public static Object getOrIssueCredential( IssueCredentialResult issResult = issuerClient.credentials().issue(issuerAid.name, cData); waitOperation(issuerClient, issResult.getOp()); - Object credential = issuerClient.credentials().get(issResult.getAcdc().getKed().get("d").toString()).get(); + Object credential = issuerClient.credentials().get(issResult.getAcdc().getKed().get("d").toString(), false).get(); return credential; } From d6aea58a505eb92b43cb7e8def122599f6dfa78b Mon Sep 17 00:00:00 2001 From: Patrick Vu Date: Mon, 3 Nov 2025 18:32:38 +0700 Subject: [PATCH 5/7] add CESRStream util --- .../signify/cesr/util/CESRStreamUtil.java | 138 ++++++++++++++++++ .../signify/e2e/VerifyCredentialTest.java | 86 +---------- 2 files changed, 144 insertions(+), 80 deletions(-) create mode 100644 src/main/java/org/cardanofoundation/signify/cesr/util/CESRStreamUtil.java diff --git a/src/main/java/org/cardanofoundation/signify/cesr/util/CESRStreamUtil.java b/src/main/java/org/cardanofoundation/signify/cesr/util/CESRStreamUtil.java new file mode 100644 index 00000000..78d30f52 --- /dev/null +++ b/src/main/java/org/cardanofoundation/signify/cesr/util/CESRStreamUtil.java @@ -0,0 +1,138 @@ +package org.cardanofoundation.signify.cesr.util; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility class for parsing and reconstructing CESR format streams. + */ +public class CESRStreamUtil { + + /** + * 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; + } + + /** + * Reconstructs a CESR format stream from parsed events and their attachments. + * @param parsedData List of maps containing "event" and "atc" keys + * @return A CESR format string with events and attachments concatenated + */ + @SuppressWarnings("unchecked") + public static String makeCESRStream(List> parsedData) { + StringBuilder cesrStream = new StringBuilder(); + + for (Map eventData : parsedData) { + Map event = (Map) eventData.get("event"); + String attachment = (String) eventData.get("atc"); + + if (event != null) { + String jsonEvent = Utils.jsonStringify(event); + cesrStream.append(jsonEvent); + } + + if (attachment != null && !attachment.isEmpty()) { + cesrStream.append(attachment); + } + } + + return cesrStream.toString(); + } + + /** + * Reconstructs a CESR format stream from separate lists of events and attachments. + * @param events List of event maps (VCP, ISS, ACDC events) + * @param attachments List of attachment strings corresponding to each event (can be null for events without attachments) + * @return A CESR format string with events and attachments concatenated + * @throws IllegalArgumentException if events and attachments lists have different sizes + */ + public static String makeCESRStream(List> events, List attachments) { + if (events.size() != attachments.size()) { + throw new IllegalArgumentException( + "Events and attachments lists must have the same size. " + + "Events: " + events.size() + ", Attachments: " + attachments.size() + ); + } + + List> parsedData = new ArrayList<>(); + for (int i = 0; i < events.size(); i++) { + Map eventMap = new LinkedHashMap<>(); + eventMap.put("event", events.get(i)); + eventMap.put("atc", attachments.get(i) != null ? attachments.get(i) : ""); + parsedData.add(eventMap); + } + + return makeCESRStream(parsedData); + } +} diff --git a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java index d0ccafb6..39abe562 100644 --- a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java +++ b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java @@ -7,7 +7,7 @@ import org.cardanofoundation.signify.app.credentialing.registries.RegistryResult; import org.cardanofoundation.signify.app.credentialing.registries.RegistryVerifyOptions; import org.cardanofoundation.signify.cesr.Serder; -import org.cardanofoundation.signify.cesr.util.Utils; +import org.cardanofoundation.signify.cesr.util.CESRStreamUtil; import org.cardanofoundation.signify.e2e.utils.ResolveEnv; import org.cardanofoundation.signify.e2e.utils.TestSteps; import org.cardanofoundation.signify.e2e.utils.TestUtils; @@ -153,7 +153,7 @@ public void verify_credential_workflow() throws Exception { String credentialCesr = issuerClient.credentials().get(credId).get(); // Parse CESR data to extract VCP, ISS, and ACDC events - List> cesrData = parseCESRData(credentialCesr); + List> cesrData = CESRStreamUtil.parseCESRData(credentialCesr); for (Map eventData : cesrData) { Map event = (Map) eventData.get("event"); @@ -300,7 +300,7 @@ public void verify_credential_workflow() throws Exception { leCredentialCesr = leCredentialCesrOpt.get(); // Parse CESR data to extract VCP, ISS, and ACDC events for LE credential - List> leCesrData = parseCESRData(leCredentialCesr); + List> leCesrData = CESRStreamUtil.parseCESRData(leCredentialCesr); // Collect all VCP, ISS, and ACDC events for chained credential verification List> allVcpEvents = new ArrayList<>(); @@ -377,7 +377,7 @@ public void verify_credential_workflow() throws Exception { System.out.println("\n=== Verifying All VCP Events in Chain ==="); - List> leCesrData = parseCESRData(leCredentialCesr); + List> leCesrData = CESRStreamUtil.parseCESRData(leCredentialCesr); List> allVcpEvents = new ArrayList<>(); List allVcpAttachments = new ArrayList<>(); @@ -423,7 +423,7 @@ public void verify_credential_workflow() throws Exception { 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> leCesrData = CESRStreamUtil.parseCESRData(leCredentialCesr); List> allIssEvents = new ArrayList<>(); List allIssAttachments = new ArrayList<>(); @@ -477,78 +477,4 @@ 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 dd52520b056039be0b63bbdbd08ef63e489009af Mon Sep 17 00:00:00 2001 From: Florian Schumann Date: Wed, 18 Feb 2026 10:35:22 +0100 Subject: [PATCH 6/7] chore: dev release --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 45db296c..55a0ef6b 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ repositories { } group = 'org.cardanofoundation' -version = '0.1.2-SNAPSHOT' +version = '0.1.2-PR62-d6aea58' def commit_id = getCheckedOutGitCommitHash() if (project.version.endsWith("-SNAPSHOT")) { From 51885eeba9b902b6570dfda0366ec49f533d3d9f Mon Sep 17 00:00:00 2001 From: Florian Schumann Date: Wed, 18 Feb 2026 11:13:12 +0100 Subject: [PATCH 7/7] revert: chore: dev release This reverts commit dd52520b056039be0b63bbdbd08ef63e489009af. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 55a0ef6b..45db296c 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ repositories { } group = 'org.cardanofoundation' -version = '0.1.2-PR62-d6aea58' +version = '0.1.2-SNAPSHOT' def commit_id = getCheckedOutGitCommitHash() if (project.version.endsWith("-SNAPSHOT")) {