Added v2 version of W3C Verifiable Credential
This commit is contained in:
parent
4b9b1d4147
commit
50b29ee167
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user