diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java index 3886f84..dc1b3e3 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java @@ -8,6 +8,8 @@ import static org.oneedtech.inspect.vc.VerifiableCredential.Type.VerifiablePrese import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; + +import java.net.URI; import java.util.Collections; import java.util.List; import java.util.Map; @@ -26,18 +28,19 @@ import org.oneedtech.inspect.vc.util.JsonNodeUtil; */ public class VerifiableCredential extends Credential { final VerifiableCredential.Type credentialType; + final VCVersion version; protected VerifiableCredential( Resource resource, JsonNode data, String jwt, Map schemas, - String issuedOnPropertyName, - String expiresAtPropertyName) { - super(ID, resource, data, jwt, schemas, issuedOnPropertyName, expiresAtPropertyName); + VCVersion version) { + super(ID, resource, data, jwt, schemas, version.issuanceDateField, version.expirationDateField); JsonNode typeNode = jsonData.get("type"); this.credentialType = VerifiableCredential.Type.valueOf(typeNode); + this.version = version; } public CredentialEnum getCredentialType() { @@ -48,6 +51,10 @@ public class VerifiableCredential extends Credential { return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL; } + public VCVersion getVersion() { + return version; + } + private static final Map schemas = new ImmutableMap.Builder() .put(AchievementCredential, Catalog.OB_30_ANY_ACHIEVEMENTCREDENTIAL_JSON) @@ -56,17 +63,19 @@ public class VerifiableCredential extends Credential { .put(EndorsementCredential, Catalog.OB_30_ANY_ENDORSEMENTCREDENTIAL_JSON) .build(); - private static final Map, List> contextMap = + public static final String JSONLD_CONTEXT_W3C_CREDENTIALS_V2 = "https://www.w3.org/ns/credentials/v2"; + + private static final Map, List> contextMap = new ImmutableMap.Builder, List>() .put( Set.of(Type.OpenBadgeCredential, AchievementCredential, EndorsementCredential), List.of( - "https://www.w3.org/ns/credentials/v2", + JSONLD_CONTEXT_W3C_CREDENTIALS_V2, "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json")) .put( Set.of(ClrCredential), List.of( - "https://www.w3.org/ns/credentials/v2", + JSONLD_CONTEXT_W3C_CREDENTIALS_V2, "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json")) .build(); @@ -82,7 +91,7 @@ public class VerifiableCredential extends Credential { "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json", List.of("https://purl.imsglobal.org/spec/clr/v2p0/context.json")) .put( - "https://www.w3.org/ns/credentials/v2", + JSONLD_CONTEXT_W3C_CREDENTIALS_V2, List.of("https://www.w3.org/2018/credentials/v1")) .build(); @@ -183,20 +192,39 @@ public class VerifiableCredential extends Credential { .toString(); } + public static enum VCVersion { + VCDMv2p0(ISSUED_ON_PROPERTY_NAME_V20, EXPIRES_AT_PROPERTY_NAME_V20), + VCDMv1p1(ISSUED_ON_PROPERTY_NAME_V11, EXPIRES_AT_PROPERTY_NAME_V11); + + final String issuanceDateField; + final String expirationDateField; + + VCVersion(String issuanceDateField, String expirationDateField) { + this.issuanceDateField = issuanceDateField; + this.expirationDateField = expirationDateField; + } + + static VCVersion of(JsonNode context) { + if (JsonNodeUtil.asNodeList(context) + .stream() + .anyMatch(node -> node.isTextual() && node.asText().equals(JSONLD_CONTEXT_W3C_CREDENTIALS_V2)) ) + return VCDMv2p0; + + return VCDMv1p1; + } + } + public static class Builder extends Credential.Builder { @Override public VerifiableCredential build() { - boolean is2p0VC = JsonNodeUtil.asNodeList(getJsonData().get("@context")) - .stream() - .anyMatch(node -> node.isTextual() && node.asText().equals("https://www.w3.org/ns/credentials/v2")); + VCVersion version = VCVersion.of(getJsonData().get("@context")); return new VerifiableCredential( getResource(), getJsonData(), getJwt(), schemas, - is2p0VC ? ISSUED_ON_PROPERTY_NAME_V20 : ISSUED_ON_PROPERTY_NAME_V11, - is2p0VC ? EXPIRES_AT_PROPERTY_NAME_V20 : EXPIRES_AT_PROPERTY_NAME_V11); + version); } } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVCHolder.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVCHolder.java index e6e2b34..cc27088 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVCHolder.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVCHolder.java @@ -1,23 +1,33 @@ package org.oneedtech.inspect.vc; +import java.io.StringReader; import java.util.List; import org.oneedtech.inspect.vc.jsonld.JsonLDObjectUtils; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; -import com.danubetech.verifiablecredentials.VerifiableCredential; - import info.weboftrust.ldsignatures.LdProof; /** * Holder for W3C's Verifiable Credential */ public class W3CVCHolder { - private VerifiableCredential credential; + private com.danubetech.verifiablecredentials.VerifiableCredential credential; public W3CVCHolder(VerifiableCredential credential) { - this.credential = credential; - credential.setDocumentLoader(new CachingDocumentLoader()); + switch (credential.version) { + case VCDMv1p1: + this.credential = com.danubetech.verifiablecredentials.VerifiableCredential + .fromJson(new StringReader(credential.getJson().toString())); + break; + case VCDMv2p0: + this.credential = W3CVerifiableCredentialDM2 + .fromJson(new StringReader(credential.getJson().toString())); + break; + default: + throw new IllegalArgumentException("Unsupported version: " + credential.version); + } + this.credential.setDocumentLoader(new CachingDocumentLoader()); } /** @@ -30,7 +40,7 @@ public class W3CVCHolder { return JsonLDObjectUtils.getListFromJsonLDObject(LdProof.class, credential); } - public VerifiableCredential getCredential() { + public com.danubetech.verifiablecredentials.VerifiableCredential getCredential() { return credential; } } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVerifiableCredentialDM2.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVerifiableCredentialDM2.java new file mode 100644 index 0000000..cea520c --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/W3CVerifiableCredentialDM2.java @@ -0,0 +1,22 @@ +package org.oneedtech.inspect.vc; + +import java.net.URI; +import java.util.Date; + +import foundation.identity.jsonld.JsonLDUtils; + +public class W3CVerifiableCredentialDM2 extends com.danubetech.verifiablecredentials.VerifiableCredential { + public static final URI[] DEFAULT_JSONLD_CONTEXTS = { URI.create(VerifiableCredential.JSONLD_CONTEXT_W3C_CREDENTIALS_V2) }; + + public Date getValidFrom() { + return JsonLDUtils.stringToDate(JsonLDUtils.jsonLdGetString(this.getJsonObject(), JSONLD_TERM_VALIDFROM)); + } + + public Date getValidUntil() { + return JsonLDUtils.stringToDate(JsonLDUtils.jsonLdGetString(this.getJsonObject(), JSONLD_TERM_VALIDUNTIL)); + } + + private static final String JSONLD_TERM_VALIDFROM = "validFrom"; + private static final String JSONLD_TERM_VALIDUNTIL = "validUntil"; + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index 2b57b1e..692efce 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -1,34 +1,30 @@ package org.oneedtech.inspect.vc.probe; -import java.io.StringReader; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Optional; - -import org.oneedtech.inspect.core.probe.Probe; -import org.oneedtech.inspect.core.probe.RunContext; -import org.oneedtech.inspect.core.report.ReportItems; -import org.oneedtech.inspect.vc.VerifiableCredential; -import org.oneedtech.inspect.vc.W3CVCHolder; -import org.oneedtech.inspect.vc.verification.Ed25519Signature2022LdVerifier; - import com.apicatalog.jsonld.StringUtils; import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.loader.DocumentLoaderOptions; import com.apicatalog.multibase.Multibase; import com.apicatalog.multicodec.Multicodec; import com.apicatalog.multicodec.Multicodec.Codec; - import info.weboftrust.ldsignatures.LdProof; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; import info.weboftrust.ldsignatures.verifier.LdVerifier; import jakarta.json.JsonArray; import jakarta.json.JsonObject; -import jakarta.json.JsonString; import jakarta.json.JsonStructure; import jakarta.json.JsonValue; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Optional; +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.vc.VerifiableCredential; +import org.oneedtech.inspect.vc.W3CVCHolder; +import org.oneedtech.inspect.vc.verification.Ed25519Signature2022LdVerifier; +import org.oneedtech.inspect.vc.verification.Ed25519Signature2022VCDM20LdVerifier; /** * A Probe that verifies a credential's embedded proof. @@ -37,244 +33,275 @@ import jakarta.json.JsonValue; */ public class EmbeddedProofProbe extends Probe { - private final static List ALLOWED_CRYPTOSUITES = List.of("eddsa-2022", "eddsa-rdfc-2022"); + private static final List ALLOWED_CRYPTOSUITES = List.of("eddsa-2022", "eddsa-rdfc-2022"); - public EmbeddedProofProbe() { - super(ID); - } + public EmbeddedProofProbe() { + super(ID); + } - /* - * Using verifiable-credentials-java from Danubetech - * (https://github.com/danubetech/verifiable-credentials-java) - */ - @Override - public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception { + /* + * Using verifiable-credentials-java from Danubetech + * (https://github.com/danubetech/verifiable-credentials-java) + */ + @Override + public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception { - W3CVCHolder credentialHolder = new W3CVCHolder(com.danubetech.verifiablecredentials.VerifiableCredential - .fromJson(new StringReader(crd.getJson().toString()))); + W3CVCHolder credentialHolder = new W3CVCHolder(crd); - List proofs = credentialHolder.getProofs(); - if (proofs == null || proofs.size() == 0) { - return error("The verifiable credential is missing a proof.", ctx); - } + List proofs = credentialHolder.getProofs(); + if (proofs == null || proofs.size() == 0) { + return error("The verifiable credential is missing a proof.", ctx); + } - // get proof of standard type and purpose - Optional selectedProof = proofs.stream() - .filter(proof -> proof.getProofPurpose().equals("assertionMethod")) - .filter(proof -> proof.isType("Ed25519Signature2020") || - (proof.isType("DataIntegrityProof") && proof.getJsonObject().containsKey("cryptosuite") && ALLOWED_CRYPTOSUITES.contains(proof.getJsonObject().get("cryptosuite")))) - .findFirst(); + // get proof of standard type and purpose + Optional selectedProof = + proofs.stream() + .filter(proof -> proof.getProofPurpose().equals("assertionMethod")) + .filter( + proof -> + proof.isType("Ed25519Signature2020") + || (proof.isType("DataIntegrityProof") + && proof.getJsonObject().containsKey("cryptosuite") + && ALLOWED_CRYPTOSUITES.contains( + proof.getJsonObject().get("cryptosuite")))) + .findFirst(); - if (!selectedProof.isPresent()) { - return error("No proof with type any of (\"Ed25519Signature2020\", \"DataIntegrityProof\" with cryptosuite attr of \"eddsa-rdfc-2022\" or \"eddsa-2022\") or proof purpose \"assertionMethod\" found", ctx); - } + if (!selectedProof.isPresent()) { + return error( + "No proof with type any of (\"Ed25519Signature2020\", \"DataIntegrityProof\" with" + + " cryptosuite attr of \"eddsa-rdfc-2022\" or \"eddsa-2022\") or proof purpose" + + " \"assertionMethod\" found", + ctx); + } - LdProof proof = selectedProof.get(); + LdProof proof = selectedProof.get(); - URI method = proof.getVerificationMethod(); + URI method = proof.getVerificationMethod(); - // The verification method must dereference to an Ed25519VerificationKey2020. - // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key - // from the Ed25519VerificationKey2020 (32 bytes). - // - // Formats accepted: - // - // [controller]#[publicKeyMultibase] - // did:key:[publicKeyMultibase] - // did:web:[url-encoded domain-name][:path]* - // http/s://[location of a Ed25519VerificationKey2020 document] - // http/s://[location of a controller document with a 'verificationMethod' with - // a Ed25519VerificationKey2020] + // The verification method must dereference to an Ed25519VerificationKey2020. + // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key + // from the Ed25519VerificationKey2020 (32 bytes). + // + // Formats accepted: + // + // [controller]#[publicKeyMultibase] + // did:key:[publicKeyMultibase] + // did:web:[url-encoded domain-name][:path]* + // http/s://[location of a Ed25519VerificationKey2020 document] + // http/s://[location of a controller document with a 'verificationMethod' with + // a Ed25519VerificationKey2020] - String publicKeyMultibase; - String controller = null; + String publicKeyMultibase; + String controller = null; - publicKeyMultibase = method.toString(); + publicKeyMultibase = method.toString(); - if (method.getFragment() != null && IsValidPublicKeyMultibase(method.getFragment())) { - publicKeyMultibase = method.getFragment(); - controller = method.toString().substring(0, method.toString().indexOf("#")); - } else { - if (StringUtils.isBlank(method.getScheme())) { - return error("The verification method must be a valid URI (missing scheme)", ctx); - } else if (method.getScheme().equals("did")) { - if (method.getSchemeSpecificPart().startsWith("key:")) { - publicKeyMultibase = method.getSchemeSpecificPart().substring("key:".length()); - } else if (method.getSchemeSpecificPart().startsWith("web:")) { - String methodSpecificId = method.getRawSchemeSpecificPart().substring("web:".length()); + if (method.getFragment() != null && IsValidPublicKeyMultibase(method.getFragment())) { + publicKeyMultibase = method.getFragment(); + controller = method.toString().substring(0, method.toString().indexOf("#")); + } else { + if (StringUtils.isBlank(method.getScheme())) { + return error("The verification method must be a valid URI (missing scheme)", ctx); + } else if (method.getScheme().equals("did")) { + if (method.getSchemeSpecificPart().startsWith("key:")) { + publicKeyMultibase = method.getSchemeSpecificPart().substring("key:".length()); + } else if (method.getSchemeSpecificPart().startsWith("web:")) { + String methodSpecificId = method.getRawSchemeSpecificPart().substring("web:".length()); - // read algorithm at https://w3c-ccg.github.io/did-method-web/#read-resolve. - // Steps in comments + // read algorithm at https://w3c-ccg.github.io/did-method-web/#read-resolve. + // Steps in comments - // 1. Replace ":" with "/" in the method specific identifier to obtain the fully - // qualified domain name and optional path. - methodSpecificId = methodSpecificId.replaceAll(":", "/"); + // 1. Replace ":" with "/" in the method specific identifier to obtain the fully + // qualified domain name and optional path. + methodSpecificId = methodSpecificId.replaceAll(":", "/"); - // 2. If the domain contains a port percent decode the colon. - String portPercentEncoded = URLEncoder.encode(":", Charset.forName("UTF-8")); - int index = methodSpecificId.indexOf(portPercentEncoded); - if (index >= 0 && index < methodSpecificId.indexOf("/")) { - methodSpecificId = methodSpecificId.replace(portPercentEncoded, ":"); - } + // 2. If the domain contains a port percent decode the colon. + String portPercentEncoded = URLEncoder.encode(":", Charset.forName("UTF-8")); + int index = methodSpecificId.indexOf(portPercentEncoded); + if (index >= 0 && index < methodSpecificId.indexOf("/")) { + methodSpecificId = methodSpecificId.replace(portPercentEncoded, ":"); + } - // 3. Generate an HTTPS URL to the expected location of the DID document by - // prepending https://. - URI uri = new URI("https://" + methodSpecificId); + // 3. Generate an HTTPS URL to the expected location of the DID document by + // prepending https://. + URI uri = new URI("https://" + methodSpecificId); - // 4. If no path has been specified in the URL, append /.well-known. - if (uri.getPath() == null) { - uri = uri.resolve("/well-known"); - } + // 4. If no path has been specified in the URL, append /.well-known. + if (uri.getPath() == null) { + uri = uri.resolve("/well-known"); + } - // 5. Append /did.json to complete the URL. - uri = uri.resolve(uri.getPath() + "/did.json"); + // 5. Append /did.json to complete the URL. + uri = uri.resolve(uri.getPath() + "/did.json"); - // 6. Perform an HTTP GET request to the URL using an agent that can - // successfully negotiate a secure HTTPS connection, which enforces the security - // requirements as described in 2.6 Security and privacy considerations. - // 7. When performing the DNS resolution during the HTTP GET request, the client - // SHOULD utilize [RFC8484] in order to prevent tracking of the identity being - // resolved. - Optional keyStructure; - try { - Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(uri, - new DocumentLoaderOptions()); - keyStructure = keyDocument.getJsonContent(); - } catch (Exception e) { - return error("Key document not found at " + method + ". URI: " + uri - + " doesn't return a valid document. Reason: " + e.getMessage() + " ", ctx); - } - if (keyStructure.isEmpty()) { - return error("Key document not found at " + method + ". URI: " + uri - + " doesn't return a valid document. Reason: The document is empty.", ctx); - } + // 6. Perform an HTTP GET request to the URL using an agent that can + // successfully negotiate a secure HTTPS connection, which enforces the security + // requirements as described in 2.6 Security and privacy considerations. + // 7. When performing the DNS resolution during the HTTP GET request, the client + // SHOULD utilize [RFC8484] in order to prevent tracking of the identity being + // resolved. + Optional keyStructure; + try { + Document keyDocument = + credentialHolder + .getCredential() + .getDocumentLoader() + .loadDocument(uri, new DocumentLoaderOptions()); + keyStructure = keyDocument.getJsonContent(); + } catch (Exception e) { + return error( + "Key document not found at " + + method + + ". URI: " + + uri + + " doesn't return a valid document. Reason: " + + e.getMessage() + + " ", + ctx); + } + if (keyStructure.isEmpty()) { + return error( + "Key document not found at " + + method + + ". URI: " + + uri + + " doesn't return a valid document. Reason: The document is empty.", + ctx); + } - // check did in "assertionMethod" - JsonArray assertionMethod = keyStructure.get().asJsonObject() - .getJsonArray("assertionMethod"); - if (assertionMethod == null) { - return error("Document doesn't have a list of assertion methods at URI: " + uri, ctx); - } else { - Boolean anyMatch = false; - for(int i = 0; i < assertionMethod.size(); i++) { - String assertionMethodValue = assertionMethod.getString(i); - if (assertionMethodValue.equals(method.toString())) { - anyMatch = true; - break; - } - } - if (!anyMatch) { - return error("Assertion method " + method + " not found in DID document.", ctx); - } - } + // check did in "assertionMethod" + JsonArray assertionMethod = + keyStructure.get().asJsonObject().getJsonArray("assertionMethod"); + if (assertionMethod == null) { + return error("Document doesn't have a list of assertion methods at URI: " + uri, ctx); + } else { + Boolean anyMatch = false; + for (int i = 0; i < assertionMethod.size(); i++) { + String assertionMethodValue = assertionMethod.getString(i); + if (assertionMethodValue.equals(method.toString())) { + anyMatch = true; + break; + } + } + if (!anyMatch) { + return error("Assertion method " + method + " not found in DID document.", ctx); + } + } - // get keys from "verificationMethod" - JsonArray keyVerificationMethod = keyStructure.get().asJsonObject() - .getJsonArray("verificationMethod"); - if (keyVerificationMethod == null) { - return error("Document doesn't have a list of verification methods at URI: " + uri, ctx); - } - Optional verificationMethodMaybe = keyVerificationMethod.stream() - .filter(n -> n.asJsonObject().getString("id").equals(method.toString())) - .findFirst(); - if (verificationMethodMaybe.isEmpty()) { - return error("Verification method " + method + " not found in DID document.", ctx); - } - JsonObject verificationMethod = verificationMethodMaybe.get().asJsonObject(); - // assuming a Ed25519VerificationKey2020 document - controller = verificationMethod.getString("controller"); - publicKeyMultibase = verificationMethod.getString("publicKeyMultibase"); + // get keys from "verificationMethod" + JsonArray keyVerificationMethod = + keyStructure.get().asJsonObject().getJsonArray("verificationMethod"); + if (keyVerificationMethod == null) { + return error( + "Document doesn't have a list of verification methods at URI: " + uri, ctx); + } + Optional verificationMethodMaybe = + keyVerificationMethod.stream() + .filter(n -> n.asJsonObject().getString("id").equals(method.toString())) + .findFirst(); + if (verificationMethodMaybe.isEmpty()) { + return error("Verification method " + method + " not found in DID document.", ctx); + } + JsonObject verificationMethod = verificationMethodMaybe.get().asJsonObject(); + // assuming a Ed25519VerificationKey2020 document + controller = verificationMethod.getString("controller"); + publicKeyMultibase = verificationMethod.getString("publicKeyMultibase"); - } else { - return error("Unknown verification method: " + method, ctx); - } - } else if (method.getScheme().equals("http") || method.getScheme().equals("https")) { - try { - Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(method, - new DocumentLoaderOptions()); - Optional keyStructure = keyDocument.getJsonContent(); - if (keyStructure.isEmpty()) { - return error("Key document not found at " + method, ctx); - } + } else { + return error("Unknown verification method: " + method, ctx); + } + } else if (method.getScheme().equals("http") || method.getScheme().equals("https")) { + try { + Document keyDocument = + credentialHolder + .getCredential() + .getDocumentLoader() + .loadDocument(method, new DocumentLoaderOptions()); + Optional keyStructure = keyDocument.getJsonContent(); + if (keyStructure.isEmpty()) { + return error("Key document not found at " + method, ctx); + } - // First look for a Ed25519VerificationKey2020 document - controller = keyStructure.get().asJsonObject().getString("controller"); - if (StringUtils.isBlank(controller)) { - // Then look for a controller document (e.g. DID Document) with a - // 'verificationMethod' - // that is a Ed25519VerificationKey2020 document - JsonObject keyVerificationMethod = keyStructure.get().asJsonObject() - .getJsonObject("verificationMethod"); - if (keyVerificationMethod.isEmpty()) { - return error("Cannot parse key document from " + method, ctx); - } - controller = keyVerificationMethod.getString("controller"); - publicKeyMultibase = keyVerificationMethod.getString("publicKeyMultibase"); - } else { - publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase"); - } + // First look for a Ed25519VerificationKey2020 document + controller = keyStructure.get().asJsonObject().getString("controller"); + if (StringUtils.isBlank(controller)) { + // Then look for a controller document (e.g. DID Document) with a + // 'verificationMethod' + // that is a Ed25519VerificationKey2020 document + JsonObject keyVerificationMethod = + keyStructure.get().asJsonObject().getJsonObject("verificationMethod"); + if (keyVerificationMethod.isEmpty()) { + return error("Cannot parse key document from " + method, ctx); + } + controller = keyVerificationMethod.getString("controller"); + publicKeyMultibase = keyVerificationMethod.getString("publicKeyMultibase"); + } else { + publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase"); + } - } catch (Exception e) { - return error("Invalid verification key URL: " + e.getMessage(), ctx); - } - } else { - return error("Unknown verification method scheme: " + method.getScheme(), ctx); - } - } + } catch (Exception e) { + return error("Invalid verification key URL: " + e.getMessage(), ctx); + } + } else { + return error("Unknown verification method scheme: " + method.getScheme(), ctx); + } + } - // Decode the Multibase to Multicodec and check that it is an Ed25519 public key - // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 - byte[] publicKeyMulticodec; - try { - publicKeyMulticodec = Multibase.decode(publicKeyMultibase); - if (publicKeyMulticodec[0] != (byte) 0xed || publicKeyMulticodec[1] != (byte) 0x01) { - return error("Verification method does not contain an Ed25519 public key", ctx); - } - } catch (Exception e) { - return error("Invalid public key: " + e.getMessage(), ctx); - } + // Decode the Multibase to Multicodec and check that it is an Ed25519 public key + // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 + byte[] publicKeyMulticodec; + try { + publicKeyMulticodec = Multibase.decode(publicKeyMultibase); + if (publicKeyMulticodec[0] != (byte) 0xed || publicKeyMulticodec[1] != (byte) 0x01) { + return error("Verification method does not contain an Ed25519 public key", ctx); + } + } catch (Exception e) { + return error("Invalid public key: " + e.getMessage(), ctx); + } - if (controller != null) { - if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) { - return error("Key controller does not match issuer: " + credentialHolder.getCredential().getIssuer(), - ctx); - } - } + if (controller != null) { + if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) { + return error( + "Key controller does not match issuer: " + credentialHolder.getCredential().getIssuer(), + ctx); + } + } - // Extract the publicKey bytes from the Multicodec - byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); + // Extract the publicKey bytes from the Multicodec + byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); - // choose verifier - LdVerifier verifier = getVerifier(proof, publicKey); + // choose verifier + LdVerifier verifier = getVerifier(proof, publicKey, crd); - try { - boolean verify = verifier.verify(credentialHolder.getCredential(), proof); - if (!verify) { - return error("Embedded proof verification failed.", ctx); - } - } catch (Exception e) { - return fatal("Embedded proof verification failed: " + e.getMessage(), ctx); - } + try { + boolean verify = verifier.verify(credentialHolder.getCredential(), proof); + if (!verify) { + return error("Embedded proof verification failed.", ctx); + } + } catch (Exception e) { + return fatal("Embedded proof verification failed: " + e.getMessage(), ctx); + } - return success(ctx); - } + return success(ctx); + } - private LdVerifier getVerifier(LdProof proof, byte[] publicKey) { - return proof.isType("Ed25519Signature2020") - ? new Ed25519Signature2020LdVerifier(publicKey) - : new Ed25519Signature2022LdVerifier(publicKey); - } + private LdVerifier getVerifier(LdProof proof, byte[] publicKey, VerifiableCredential crd) { + return proof.isType("Ed25519Signature2020") + ? new Ed25519Signature2020LdVerifier(publicKey) + : crd.getVersion() == VerifiableCredential.VCVersion.VCDMv1p1 + ? new Ed25519Signature2022LdVerifier(publicKey) + : new Ed25519Signature2022VCDM20LdVerifier(publicKey); + } - private Boolean IsValidPublicKeyMultibase(String publicKeyMultibase) { - try { - byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase); - byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); - return publicKey.length == 32; - } catch (Exception e) { - return false; - } + private Boolean IsValidPublicKeyMultibase(String publicKeyMultibase) { + try { + byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase); + byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); + return publicKey.length == 32; + } catch (Exception e) { + return false; + } + } - } - - public static final String ID = EmbeddedProofProbe.class.getSimpleName(); + public static final String ID = EmbeddedProofProbe.class.getSimpleName(); }