diff --git a/docker-compose.yaml b/docker-compose.yaml index 114ea49..2267d47 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 diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java index bc434d1..08cf7ee 100644 --- a/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/credentials/Credentials.java @@ -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,33 @@ 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 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 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 (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); + return Operation.fromObject(Utils.fromJson(response.body(), Map.class)); + } } diff --git a/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java index 41626b5..3ef5839 100644 --- a/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java +++ b/src/main/java/org/cardanofoundation/signify/app/credentialing/registries/Registries.java @@ -2,6 +2,7 @@ import org.cardanofoundation.signify.app.clienting.SignifyClient; import org.cardanofoundation.signify.app.coring.Coring; +import org.cardanofoundation.signify.app.coring.Operation; import org.cardanofoundation.signify.app.habery.TraitCodex; import org.cardanofoundation.signify.cesr.Keeping; import org.cardanofoundation.signify.cesr.Serder; @@ -165,4 +166,28 @@ public Object rename(String name, String registryName, String newName) throws IO HttpResponse response = this.client.fetch(path, method, data); return Utils.fromJson(response.body(), Object.class); } + + /** + * Verify a registry with optional attachment + * + * @param vcp the VCP (Verifiable Credential Protocol) data to verify + * @param atc the optional attachment data (metadata) + * @return Operation containing the verification result + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the operation is interrupted + * @throws LibsodiumException if a sodium exception occurs + */ + public Operation verify(Serder vcp, String atc) throws IOException, InterruptedException, LibsodiumException { + final String path = "/registries/verify"; + final String method = "POST"; + + Map body = new LinkedHashMap<>(); + body.put("vcp", vcp.getKed()); + if (atc != null && !atc.isEmpty()) { + body.put("atc", atc); + } + + HttpResponse response = this.client.fetch(path, method, body); + return Operation.fromObject(Utils.fromJson(response.body(), Map.class)); + } } diff --git a/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java new file mode 100644 index 0000000..5a0ded2 --- /dev/null +++ b/src/test/java/org/cardanofoundation/signify/e2e/VerifyCredentialTest.java @@ -0,0 +1,533 @@ +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.util.*; +import java.util.concurrent.Callable; +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, 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); + holderAid = aids.get(2); + legalEntityAid = aids.get(3); + } + + @BeforeEach + public void getContact() { + getOrCreateContactAsync( + 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(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); + + 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", () -> { + 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); + + 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; + }); + + 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); + + 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); + } + } + } + } + + // 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; + }); + + 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")); + } + } + + // 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.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 ==="); + + // 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()); + } + + // 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; + }); + } + + 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