Added v2 version of W3C Verifiable Credential

This commit is contained in:
Xavi Aracil 2024-03-21 18:00:35 +01:00
parent 4b9b1d4147
commit 50b29ee167
4 changed files with 328 additions and 241 deletions

View File

@ -8,6 +8,8 @@ import static org.oneedtech.inspect.vc.VerifiableCredential.Type.VerifiablePrese
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -26,18 +28,19 @@ import org.oneedtech.inspect.vc.util.JsonNodeUtil;
*/ */
public class VerifiableCredential extends Credential { public class VerifiableCredential extends Credential {
final VerifiableCredential.Type credentialType; final VerifiableCredential.Type credentialType;
final VCVersion version;
protected VerifiableCredential( protected VerifiableCredential(
Resource resource, Resource resource,
JsonNode data, JsonNode data,
String jwt, String jwt,
Map<CredentialEnum, SchemaKey> schemas, Map<CredentialEnum, SchemaKey> schemas,
String issuedOnPropertyName, VCVersion version) {
String expiresAtPropertyName) { super(ID, resource, data, jwt, schemas, version.issuanceDateField, version.expirationDateField);
super(ID, resource, data, jwt, schemas, issuedOnPropertyName, expiresAtPropertyName);
JsonNode typeNode = jsonData.get("type"); JsonNode typeNode = jsonData.get("type");
this.credentialType = VerifiableCredential.Type.valueOf(typeNode); this.credentialType = VerifiableCredential.Type.valueOf(typeNode);
this.version = version;
} }
public CredentialEnum getCredentialType() { public CredentialEnum getCredentialType() {
@ -48,6 +51,10 @@ public class VerifiableCredential extends Credential {
return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL; return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL;
} }
public VCVersion getVersion() {
return version;
}
private static final Map<CredentialEnum, SchemaKey> schemas = private static final Map<CredentialEnum, SchemaKey> schemas =
new ImmutableMap.Builder<CredentialEnum, SchemaKey>() new ImmutableMap.Builder<CredentialEnum, SchemaKey>()
.put(AchievementCredential, Catalog.OB_30_ANY_ACHIEVEMENTCREDENTIAL_JSON) .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) .put(EndorsementCredential, Catalog.OB_30_ANY_ENDORSEMENTCREDENTIAL_JSON)
.build(); .build();
private static final Map<Set<VerifiableCredential.Type>, List<String>> contextMap = public static final String JSONLD_CONTEXT_W3C_CREDENTIALS_V2 = "https://www.w3.org/ns/credentials/v2";
private static final Map<Set<VerifiableCredential.Type>, List<String>> contextMap =
new ImmutableMap.Builder<Set<VerifiableCredential.Type>, List<String>>() new ImmutableMap.Builder<Set<VerifiableCredential.Type>, List<String>>()
.put( .put(
Set.of(Type.OpenBadgeCredential, AchievementCredential, EndorsementCredential), Set.of(Type.OpenBadgeCredential, AchievementCredential, EndorsementCredential),
List.of( 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")) "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"))
.put( .put(
Set.of(ClrCredential), Set.of(ClrCredential),
List.of( 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/clr/v2p0/context-2.0.1.json",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json")) "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"))
.build(); .build();
@ -82,7 +91,7 @@ public class VerifiableCredential extends Credential {
"https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json", "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json",
List.of("https://purl.imsglobal.org/spec/clr/v2p0/context.json")) List.of("https://purl.imsglobal.org/spec/clr/v2p0/context.json"))
.put( .put(
"https://www.w3.org/ns/credentials/v2", JSONLD_CONTEXT_W3C_CREDENTIALS_V2,
List.of("https://www.w3.org/2018/credentials/v1")) List.of("https://www.w3.org/2018/credentials/v1"))
.build(); .build();
@ -183,20 +192,39 @@ public class VerifiableCredential extends Credential {
.toString(); .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<VerifiableCredential> { public static class Builder extends Credential.Builder<VerifiableCredential> {
@Override @Override
public VerifiableCredential build() { public VerifiableCredential build() {
boolean is2p0VC = JsonNodeUtil.asNodeList(getJsonData().get("@context")) VCVersion version = VCVersion.of(getJsonData().get("@context"));
.stream()
.anyMatch(node -> node.isTextual() && node.asText().equals("https://www.w3.org/ns/credentials/v2"));
return new VerifiableCredential( return new VerifiableCredential(
getResource(), getResource(),
getJsonData(), getJsonData(),
getJwt(), getJwt(),
schemas, schemas,
is2p0VC ? ISSUED_ON_PROPERTY_NAME_V20 : ISSUED_ON_PROPERTY_NAME_V11, version);
is2p0VC ? EXPIRES_AT_PROPERTY_NAME_V20 : EXPIRES_AT_PROPERTY_NAME_V11);
} }
} }

View File

@ -1,23 +1,33 @@
package org.oneedtech.inspect.vc; package org.oneedtech.inspect.vc;
import java.io.StringReader;
import java.util.List; import java.util.List;
import org.oneedtech.inspect.vc.jsonld.JsonLDObjectUtils; import org.oneedtech.inspect.vc.jsonld.JsonLDObjectUtils;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import com.danubetech.verifiablecredentials.VerifiableCredential;
import info.weboftrust.ldsignatures.LdProof; import info.weboftrust.ldsignatures.LdProof;
/** /**
* Holder for W3C's Verifiable Credential * Holder for W3C's Verifiable Credential
*/ */
public class W3CVCHolder { public class W3CVCHolder {
private VerifiableCredential credential; private com.danubetech.verifiablecredentials.VerifiableCredential credential;
public W3CVCHolder(VerifiableCredential credential) { public W3CVCHolder(VerifiableCredential credential) {
this.credential = credential; switch (credential.version) {
credential.setDocumentLoader(new CachingDocumentLoader()); 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); return JsonLDObjectUtils.getListFromJsonLDObject(LdProof.class, credential);
} }
public VerifiableCredential getCredential() { public com.danubetech.verifiablecredentials.VerifiableCredential getCredential() {
return credential; return credential;
} }
} }

View File

@ -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";
}

View File

@ -1,34 +1,30 @@
package org.oneedtech.inspect.vc.probe; 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.StringUtils;
import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.Document;
import com.apicatalog.jsonld.loader.DocumentLoaderOptions; import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.apicatalog.multibase.Multibase; import com.apicatalog.multibase.Multibase;
import com.apicatalog.multicodec.Multicodec; import com.apicatalog.multicodec.Multicodec;
import com.apicatalog.multicodec.Multicodec.Codec; import com.apicatalog.multicodec.Multicodec.Codec;
import info.weboftrust.ldsignatures.LdProof; import info.weboftrust.ldsignatures.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
import info.weboftrust.ldsignatures.verifier.LdVerifier; import info.weboftrust.ldsignatures.verifier.LdVerifier;
import jakarta.json.JsonArray; import jakarta.json.JsonArray;
import jakarta.json.JsonObject; import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonStructure; import jakarta.json.JsonStructure;
import jakarta.json.JsonValue; 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. * A Probe that verifies a credential's embedded proof.
@ -37,244 +33,275 @@ import jakarta.json.JsonValue;
*/ */
public class EmbeddedProofProbe extends Probe<VerifiableCredential> { public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
private final static List<String> ALLOWED_CRYPTOSUITES = List.of("eddsa-2022", "eddsa-rdfc-2022"); private static final List<String> ALLOWED_CRYPTOSUITES = List.of("eddsa-2022", "eddsa-rdfc-2022");
public EmbeddedProofProbe() { public EmbeddedProofProbe() {
super(ID); super(ID);
} }
/* /*
* Using verifiable-credentials-java from Danubetech * Using verifiable-credentials-java from Danubetech
* (https://github.com/danubetech/verifiable-credentials-java) * (https://github.com/danubetech/verifiable-credentials-java)
*/ */
@Override @Override
public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception { public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
W3CVCHolder credentialHolder = new W3CVCHolder(com.danubetech.verifiablecredentials.VerifiableCredential W3CVCHolder credentialHolder = new W3CVCHolder(crd);
.fromJson(new StringReader(crd.getJson().toString())));
List<LdProof> proofs = credentialHolder.getProofs(); List<LdProof> proofs = credentialHolder.getProofs();
if (proofs == null || proofs.size() == 0) { if (proofs == null || proofs.size() == 0) {
return error("The verifiable credential is missing a proof.", ctx); return error("The verifiable credential is missing a proof.", ctx);
} }
// get proof of standard type and purpose // get proof of standard type and purpose
Optional<LdProof> selectedProof = proofs.stream() Optional<LdProof> selectedProof =
.filter(proof -> proof.getProofPurpose().equals("assertionMethod")) proofs.stream()
.filter(proof -> proof.isType("Ed25519Signature2020") || .filter(proof -> proof.getProofPurpose().equals("assertionMethod"))
(proof.isType("DataIntegrityProof") && proof.getJsonObject().containsKey("cryptosuite") && ALLOWED_CRYPTOSUITES.contains(proof.getJsonObject().get("cryptosuite")))) .filter(
.findFirst(); proof ->
proof.isType("Ed25519Signature2020")
|| (proof.isType("DataIntegrityProof")
&& proof.getJsonObject().containsKey("cryptosuite")
&& ALLOWED_CRYPTOSUITES.contains(
proof.getJsonObject().get("cryptosuite"))))
.findFirst();
if (!selectedProof.isPresent()) { 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); 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. // The verification method must dereference to an Ed25519VerificationKey2020.
// Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key
// from the Ed25519VerificationKey2020 (32 bytes). // from the Ed25519VerificationKey2020 (32 bytes).
// //
// Formats accepted: // Formats accepted:
// //
// [controller]#[publicKeyMultibase] // [controller]#[publicKeyMultibase]
// did:key:[publicKeyMultibase] // did:key:[publicKeyMultibase]
// did:web:[url-encoded domain-name][:path]* // did:web:[url-encoded domain-name][:path]*
// http/s://[location of a Ed25519VerificationKey2020 document] // http/s://[location of a Ed25519VerificationKey2020 document]
// http/s://[location of a controller document with a 'verificationMethod' with // http/s://[location of a controller document with a 'verificationMethod' with
// a Ed25519VerificationKey2020] // a Ed25519VerificationKey2020]
String publicKeyMultibase; String publicKeyMultibase;
String controller = null; String controller = null;
publicKeyMultibase = method.toString(); publicKeyMultibase = method.toString();
if (method.getFragment() != null && IsValidPublicKeyMultibase(method.getFragment())) { if (method.getFragment() != null && IsValidPublicKeyMultibase(method.getFragment())) {
publicKeyMultibase = method.getFragment(); publicKeyMultibase = method.getFragment();
controller = method.toString().substring(0, method.toString().indexOf("#")); controller = method.toString().substring(0, method.toString().indexOf("#"));
} else { } else {
if (StringUtils.isBlank(method.getScheme())) { if (StringUtils.isBlank(method.getScheme())) {
return error("The verification method must be a valid URI (missing scheme)", ctx); return error("The verification method must be a valid URI (missing scheme)", ctx);
} else if (method.getScheme().equals("did")) { } else if (method.getScheme().equals("did")) {
if (method.getSchemeSpecificPart().startsWith("key:")) { if (method.getSchemeSpecificPart().startsWith("key:")) {
publicKeyMultibase = method.getSchemeSpecificPart().substring("key:".length()); publicKeyMultibase = method.getSchemeSpecificPart().substring("key:".length());
} else if (method.getSchemeSpecificPart().startsWith("web:")) { } else if (method.getSchemeSpecificPart().startsWith("web:")) {
String methodSpecificId = method.getRawSchemeSpecificPart().substring("web:".length()); String methodSpecificId = method.getRawSchemeSpecificPart().substring("web:".length());
// read algorithm at https://w3c-ccg.github.io/did-method-web/#read-resolve. // read algorithm at https://w3c-ccg.github.io/did-method-web/#read-resolve.
// Steps in comments // Steps in comments
// 1. Replace ":" with "/" in the method specific identifier to obtain the fully // 1. Replace ":" with "/" in the method specific identifier to obtain the fully
// qualified domain name and optional path. // qualified domain name and optional path.
methodSpecificId = methodSpecificId.replaceAll(":", "/"); methodSpecificId = methodSpecificId.replaceAll(":", "/");
// 2. If the domain contains a port percent decode the colon. // 2. If the domain contains a port percent decode the colon.
String portPercentEncoded = URLEncoder.encode(":", Charset.forName("UTF-8")); String portPercentEncoded = URLEncoder.encode(":", Charset.forName("UTF-8"));
int index = methodSpecificId.indexOf(portPercentEncoded); int index = methodSpecificId.indexOf(portPercentEncoded);
if (index >= 0 && index < methodSpecificId.indexOf("/")) { if (index >= 0 && index < methodSpecificId.indexOf("/")) {
methodSpecificId = methodSpecificId.replace(portPercentEncoded, ":"); methodSpecificId = methodSpecificId.replace(portPercentEncoded, ":");
} }
// 3. Generate an HTTPS URL to the expected location of the DID document by // 3. Generate an HTTPS URL to the expected location of the DID document by
// prepending https://. // prepending https://.
URI uri = new URI("https://" + methodSpecificId); URI uri = new URI("https://" + methodSpecificId);
// 4. If no path has been specified in the URL, append /.well-known. // 4. If no path has been specified in the URL, append /.well-known.
if (uri.getPath() == null) { if (uri.getPath() == null) {
uri = uri.resolve("/well-known"); uri = uri.resolve("/well-known");
} }
// 5. Append /did.json to complete the URL. // 5. Append /did.json to complete the URL.
uri = uri.resolve(uri.getPath() + "/did.json"); uri = uri.resolve(uri.getPath() + "/did.json");
// 6. Perform an HTTP GET request to the URL using an agent that can // 6. Perform an HTTP GET request to the URL using an agent that can
// successfully negotiate a secure HTTPS connection, which enforces the security // successfully negotiate a secure HTTPS connection, which enforces the security
// requirements as described in 2.6 Security and privacy considerations. // requirements as described in 2.6 Security and privacy considerations.
// 7. When performing the DNS resolution during the HTTP GET request, the client // 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 // SHOULD utilize [RFC8484] in order to prevent tracking of the identity being
// resolved. // resolved.
Optional<JsonStructure> keyStructure; Optional<JsonStructure> keyStructure;
try { try {
Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(uri, Document keyDocument =
new DocumentLoaderOptions()); credentialHolder
keyStructure = keyDocument.getJsonContent(); .getCredential()
} catch (Exception e) { .getDocumentLoader()
return error("Key document not found at " + method + ". URI: " + uri .loadDocument(uri, new DocumentLoaderOptions());
+ " doesn't return a valid document. Reason: " + e.getMessage() + " ", ctx); keyStructure = keyDocument.getJsonContent();
} } catch (Exception e) {
if (keyStructure.isEmpty()) { return error(
return error("Key document not found at " + method + ". URI: " + uri "Key document not found at "
+ " doesn't return a valid document. Reason: The document is empty.", ctx); + 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" // check did in "assertionMethod"
JsonArray assertionMethod = keyStructure.get().asJsonObject() JsonArray assertionMethod =
.getJsonArray("assertionMethod"); keyStructure.get().asJsonObject().getJsonArray("assertionMethod");
if (assertionMethod == null) { if (assertionMethod == null) {
return error("Document doesn't have a list of assertion methods at URI: " + uri, ctx); return error("Document doesn't have a list of assertion methods at URI: " + uri, ctx);
} else { } else {
Boolean anyMatch = false; Boolean anyMatch = false;
for(int i = 0; i < assertionMethod.size(); i++) { for (int i = 0; i < assertionMethod.size(); i++) {
String assertionMethodValue = assertionMethod.getString(i); String assertionMethodValue = assertionMethod.getString(i);
if (assertionMethodValue.equals(method.toString())) { if (assertionMethodValue.equals(method.toString())) {
anyMatch = true; anyMatch = true;
break; break;
} }
} }
if (!anyMatch) { if (!anyMatch) {
return error("Assertion method " + method + " not found in DID document.", ctx); return error("Assertion method " + method + " not found in DID document.", ctx);
} }
} }
// get keys from "verificationMethod" // get keys from "verificationMethod"
JsonArray keyVerificationMethod = keyStructure.get().asJsonObject() JsonArray keyVerificationMethod =
.getJsonArray("verificationMethod"); keyStructure.get().asJsonObject().getJsonArray("verificationMethod");
if (keyVerificationMethod == null) { if (keyVerificationMethod == null) {
return error("Document doesn't have a list of verification methods at URI: " + uri, ctx); return error(
} "Document doesn't have a list of verification methods at URI: " + uri, ctx);
Optional<JsonValue> verificationMethodMaybe = keyVerificationMethod.stream() }
.filter(n -> n.asJsonObject().getString("id").equals(method.toString())) Optional<JsonValue> verificationMethodMaybe =
.findFirst(); keyVerificationMethod.stream()
if (verificationMethodMaybe.isEmpty()) { .filter(n -> n.asJsonObject().getString("id").equals(method.toString()))
return error("Verification method " + method + " not found in DID document.", ctx); .findFirst();
} if (verificationMethodMaybe.isEmpty()) {
JsonObject verificationMethod = verificationMethodMaybe.get().asJsonObject(); return error("Verification method " + method + " not found in DID document.", ctx);
// assuming a Ed25519VerificationKey2020 document }
controller = verificationMethod.getString("controller"); JsonObject verificationMethod = verificationMethodMaybe.get().asJsonObject();
publicKeyMultibase = verificationMethod.getString("publicKeyMultibase"); // assuming a Ed25519VerificationKey2020 document
controller = verificationMethod.getString("controller");
publicKeyMultibase = verificationMethod.getString("publicKeyMultibase");
} else { } else {
return error("Unknown verification method: " + method, ctx); return error("Unknown verification method: " + method, ctx);
} }
} else if (method.getScheme().equals("http") || method.getScheme().equals("https")) { } else if (method.getScheme().equals("http") || method.getScheme().equals("https")) {
try { try {
Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(method, Document keyDocument =
new DocumentLoaderOptions()); credentialHolder
Optional<JsonStructure> keyStructure = keyDocument.getJsonContent(); .getCredential()
if (keyStructure.isEmpty()) { .getDocumentLoader()
return error("Key document not found at " + method, ctx); .loadDocument(method, new DocumentLoaderOptions());
} Optional<JsonStructure> keyStructure = keyDocument.getJsonContent();
if (keyStructure.isEmpty()) {
return error("Key document not found at " + method, ctx);
}
// First look for a Ed25519VerificationKey2020 document // First look for a Ed25519VerificationKey2020 document
controller = keyStructure.get().asJsonObject().getString("controller"); controller = keyStructure.get().asJsonObject().getString("controller");
if (StringUtils.isBlank(controller)) { if (StringUtils.isBlank(controller)) {
// Then look for a controller document (e.g. DID Document) with a // Then look for a controller document (e.g. DID Document) with a
// 'verificationMethod' // 'verificationMethod'
// that is a Ed25519VerificationKey2020 document // that is a Ed25519VerificationKey2020 document
JsonObject keyVerificationMethod = keyStructure.get().asJsonObject() JsonObject keyVerificationMethod =
.getJsonObject("verificationMethod"); keyStructure.get().asJsonObject().getJsonObject("verificationMethod");
if (keyVerificationMethod.isEmpty()) { if (keyVerificationMethod.isEmpty()) {
return error("Cannot parse key document from " + method, ctx); return error("Cannot parse key document from " + method, ctx);
} }
controller = keyVerificationMethod.getString("controller"); controller = keyVerificationMethod.getString("controller");
publicKeyMultibase = keyVerificationMethod.getString("publicKeyMultibase"); publicKeyMultibase = keyVerificationMethod.getString("publicKeyMultibase");
} else { } else {
publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase"); publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase");
} }
} catch (Exception e) { } catch (Exception e) {
return error("Invalid verification key URL: " + e.getMessage(), ctx); return error("Invalid verification key URL: " + e.getMessage(), ctx);
} }
} else { } else {
return error("Unknown verification method scheme: " + method.getScheme(), ctx); return error("Unknown verification method scheme: " + method.getScheme(), ctx);
} }
} }
// Decode the Multibase to Multicodec and check that it is an Ed25519 public key // Decode the Multibase to Multicodec and check that it is an Ed25519 public key
// https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020
byte[] publicKeyMulticodec; byte[] publicKeyMulticodec;
try { try {
publicKeyMulticodec = Multibase.decode(publicKeyMultibase); publicKeyMulticodec = Multibase.decode(publicKeyMultibase);
if (publicKeyMulticodec[0] != (byte) 0xed || publicKeyMulticodec[1] != (byte) 0x01) { if (publicKeyMulticodec[0] != (byte) 0xed || publicKeyMulticodec[1] != (byte) 0x01) {
return error("Verification method does not contain an Ed25519 public key", ctx); return error("Verification method does not contain an Ed25519 public key", ctx);
} }
} catch (Exception e) { } catch (Exception e) {
return error("Invalid public key: " + e.getMessage(), ctx); return error("Invalid public key: " + e.getMessage(), ctx);
} }
if (controller != null) { if (controller != null) {
if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) { if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) {
return error("Key controller does not match issuer: " + credentialHolder.getCredential().getIssuer(), return error(
ctx); "Key controller does not match issuer: " + credentialHolder.getCredential().getIssuer(),
} ctx);
} }
}
// Extract the publicKey bytes from the Multicodec // Extract the publicKey bytes from the Multicodec
byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec);
// choose verifier // choose verifier
LdVerifier<?> verifier = getVerifier(proof, publicKey); LdVerifier<?> verifier = getVerifier(proof, publicKey, crd);
try { try {
boolean verify = verifier.verify(credentialHolder.getCredential(), proof); boolean verify = verifier.verify(credentialHolder.getCredential(), proof);
if (!verify) { if (!verify) {
return error("Embedded proof verification failed.", ctx); return error("Embedded proof verification failed.", ctx);
} }
} catch (Exception e) { } catch (Exception e) {
return fatal("Embedded proof verification failed: " + e.getMessage(), ctx); return fatal("Embedded proof verification failed: " + e.getMessage(), ctx);
} }
return success(ctx); return success(ctx);
} }
private LdVerifier<?> getVerifier(LdProof proof, byte[] publicKey) { private LdVerifier<?> getVerifier(LdProof proof, byte[] publicKey, VerifiableCredential crd) {
return proof.isType("Ed25519Signature2020") return proof.isType("Ed25519Signature2020")
? new Ed25519Signature2020LdVerifier(publicKey) ? new Ed25519Signature2020LdVerifier(publicKey)
: new Ed25519Signature2022LdVerifier(publicKey); : crd.getVersion() == VerifiableCredential.VCVersion.VCDMv1p1
} ? new Ed25519Signature2022LdVerifier(publicKey)
: new Ed25519Signature2022VCDM20LdVerifier(publicKey);
}
private Boolean IsValidPublicKeyMultibase(String publicKeyMultibase) { private Boolean IsValidPublicKeyMultibase(String publicKeyMultibase) {
try { try {
byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase); byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase);
byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec);
return publicKey.length == 32; return publicKey.length == 32;
} catch (Exception e) { } catch (Exception e) {
return false; return false;
} }
}
} public static final String ID = EmbeddedProofProbe.class.getSimpleName();
public static final String ID = EmbeddedProofProbe.class.getSimpleName();
} }