Merge pull request #76 from imsglc/feature/did_web_support
Added did:web verification method
This commit is contained in:
commit
c808749cc4
@ -2,6 +2,8 @@ package org.oneedtech.inspect.vc.probe;
|
|||||||
|
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@ -20,8 +22,11 @@ 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 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Probe that verifies a credential's embedded proof.
|
* A Probe that verifies a credential's embedded proof.
|
||||||
@ -41,16 +46,18 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
@Override
|
@Override
|
||||||
public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
|
public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
|
||||||
|
|
||||||
W3CVCHolder credentiaHolder = new W3CVCHolder(com.danubetech.verifiablecredentials.VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())));
|
W3CVCHolder credentialHolder = new W3CVCHolder(com.danubetech.verifiablecredentials.VerifiableCredential
|
||||||
|
.fromJson(new StringReader(crd.getJson().toString())));
|
||||||
|
|
||||||
List<LdProof> proofs = credentiaHolder.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().filter(proof -> proof.isType("Ed25519Signature2020") && proof.getProofPurpose().equals("assertionMethod"))
|
Optional<LdProof> selectedProof = proofs.stream().filter(
|
||||||
.findFirst();
|
proof -> proof.isType("Ed25519Signature2020") && proof.getProofPurpose().equals("assertionMethod"))
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
if (!selectedProof.isPresent()) {
|
if (!selectedProof.isPresent()) {
|
||||||
return error("No proof with type \"Ed25519Signature2020\" or proof purpose \"assertionMethod\" found", ctx);
|
return error("No proof with type \"Ed25519Signature2020\" or proof purpose \"assertionMethod\" found", ctx);
|
||||||
@ -70,13 +77,15 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
// 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]*
|
||||||
// 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 a Ed25519VerificationKey2020]
|
// http/s://[location of a controller document with a 'verificationMethod' with
|
||||||
|
// a Ed25519VerificationKey2020]
|
||||||
|
|
||||||
String publicKeyMultibase;
|
String publicKeyMultibase;
|
||||||
String controller = null;
|
String controller = null;
|
||||||
@ -92,12 +101,98 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
} 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:")) {
|
||||||
|
String methodSpecificId = method.getRawSchemeSpecificPart().substring("web:".length());
|
||||||
|
|
||||||
|
// 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(":", "/");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// 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<JsonStructure> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<JsonValue> 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 {
|
} 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 = credentiaHolder.getCredential().getDocumentLoader().loadDocument(method, new DocumentLoaderOptions());
|
Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(method,
|
||||||
|
new DocumentLoaderOptions());
|
||||||
Optional<JsonStructure> keyStructure = keyDocument.getJsonContent();
|
Optional<JsonStructure> keyStructure = keyDocument.getJsonContent();
|
||||||
if (keyStructure.isEmpty()) {
|
if (keyStructure.isEmpty()) {
|
||||||
return error("Key document not found at " + method, ctx);
|
return error("Key document not found at " + method, ctx);
|
||||||
@ -106,7 +201,8 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
// 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 'verificationMethod'
|
// Then look for a controller document (e.g. DID Document) with a
|
||||||
|
// 'verificationMethod'
|
||||||
// that is a Ed25519VerificationKey2020 document
|
// that is a Ed25519VerificationKey2020 document
|
||||||
JsonObject keyVerificationMethod = keyStructure.get().asJsonObject()
|
JsonObject keyVerificationMethod = keyStructure.get().asJsonObject()
|
||||||
.getJsonObject("verificationMethod");
|
.getJsonObject("verificationMethod");
|
||||||
@ -140,8 +236,9 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
if (!controller.equals(credentiaHolder.getCredential().getIssuer().toString())) {
|
if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) {
|
||||||
return error("Key controller does not match issuer: " + credentiaHolder.getCredential().getIssuer(), ctx);
|
return error("Key controller does not match issuer: " + credentialHolder.getCredential().getIssuer(),
|
||||||
|
ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +248,7 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
|
|||||||
Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey);
|
Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
boolean verify = verifier.verify(credentiaHolder.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);
|
||||||
}
|
}
|
||||||
|
@ -44,9 +44,18 @@ public class OB30Tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSimpleDidMethodJsonValid() {
|
void testSimpleDidKeyMethodJsonValid() {
|
||||||
assertDoesNotThrow(()->{
|
assertDoesNotThrow(()->{
|
||||||
Report report = validator.run(Samples.OB30.JSON.SIMPLE_DID_METHOD_JSON.asFileResource());
|
Report report = validator.run(Samples.OB30.JSON.SIMPLE_DID_KEY_METHOD_JSON.asFileResource());
|
||||||
|
if(verbose) PrintHelper.print(report, true);
|
||||||
|
assertValid(report);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSimpleDidWebMethodJsonValid() {
|
||||||
|
assertDoesNotThrow(()->{
|
||||||
|
Report report = validator.run(Samples.OB30.JSON.SIMPLE_DID_WEB_METHOD_JSON.asFileResource());
|
||||||
if(verbose) PrintHelper.print(report, true);
|
if(verbose) PrintHelper.print(report, true);
|
||||||
assertValid(report);
|
assertValid(report);
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,8 @@ public class Samples {
|
|||||||
public static final class JSON {
|
public static final class JSON {
|
||||||
public final static Sample COMPLETE_JSON = new Sample("ob30/complete.json", false);
|
public final static Sample COMPLETE_JSON = new Sample("ob30/complete.json", false);
|
||||||
public final static Sample SIMPLE_JSON = new Sample("ob30/simple.json", true);
|
public final static Sample SIMPLE_JSON = new Sample("ob30/simple.json", true);
|
||||||
public final static Sample SIMPLE_DID_METHOD_JSON = new Sample("ob30/simple-did-method.json", true);
|
public final static Sample SIMPLE_DID_KEY_METHOD_JSON = new Sample("ob30/simple-did-key-method.json", true);
|
||||||
|
public final static Sample SIMPLE_DID_WEB_METHOD_JSON = new Sample("ob30/simple-did-web-method.json", true);
|
||||||
public final static Sample SIMPLE_MULTIPLE_PROOF_JSON = new Sample("ob30/simple-multiple-proofs.json", true);
|
public final static Sample SIMPLE_MULTIPLE_PROOF_JSON = new Sample("ob30/simple-multiple-proofs.json", true);
|
||||||
public final static Sample SIMPLE_JSON_NOPROOF = new Sample("ob30/simple-noproof.json", false);
|
public final static Sample SIMPLE_JSON_NOPROOF = new Sample("ob30/simple-noproof.json", false);
|
||||||
public final static Sample SIMPLE_JSON_UNKNOWN_TYPE = new Sample("ob30/simple-err-type.json", false);
|
public final static Sample SIMPLE_JSON_UNKNOWN_TYPE = new Sample("ob30/simple-err-type.json", false);
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/2018/credentials/v1",
|
||||||
|
"https://purl.imsglobal.org/spec/ob/v3p0/context.json",
|
||||||
|
"https://purl.imsglobal.org/spec/ob/v3p0/extensions.json",
|
||||||
|
"https://w3id.org/security/suites/ed25519-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "http://dc.1edtech.org/wellspring2022/wellspring-portal/credentials/5d7b7ff6-b1d5-47d5-83df-faf22533ba8f",
|
||||||
|
"type": [
|
||||||
|
"VerifiableCredential",
|
||||||
|
"AchievementCredential"
|
||||||
|
],
|
||||||
|
"issuer": {
|
||||||
|
"id": "did:web:dc.1edtech.org:wellspring2022:wellspring-portal:org:7ad80b28-4f3f-414d-85ec-6c0684344e5c",
|
||||||
|
"type": "Profile",
|
||||||
|
"name": "Wellspring School"
|
||||||
|
},
|
||||||
|
"awardedDate": "2023-05-16T11:27:00Z",
|
||||||
|
"issuanceDate": "2023-05-16T07:18:52Z",
|
||||||
|
"name": "Simple assertion of achievement 1",
|
||||||
|
"credentialSubject": {
|
||||||
|
"id": "did:web:dc.1edtech.org:wellspring2022:wellspring-portal:learner:d4e1f6ad-9696-41cb-8729-8ff741f96c6a",
|
||||||
|
"type": "AchievementSubject",
|
||||||
|
"achievement": {
|
||||||
|
"id": "http://dc.1edtech.org/wellspring2022/wellspring-portal/achievements/c44d8939-c237-4420-902c-9af305b15e2f",
|
||||||
|
"type": "Achievement",
|
||||||
|
"achievementType": "Achievement",
|
||||||
|
"criteria": {
|
||||||
|
"narrative": "The credential must pass verification."
|
||||||
|
},
|
||||||
|
"description": "This is a test achievement.",
|
||||||
|
"name": "Achievement 1"
|
||||||
|
},
|
||||||
|
"source": {
|
||||||
|
"id": "did:web:dc.1edtech.org:wellspring2022:wellspring-portal:org:7ad80b28-4f3f-414d-85ec-6c0684344e5c",
|
||||||
|
"type": "Profile",
|
||||||
|
"name": "Wellspring School"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"proof": {
|
||||||
|
"type": "Ed25519Signature2020",
|
||||||
|
"created": "2023-05-16T07:18:52Z",
|
||||||
|
"proofPurpose": "assertionMethod",
|
||||||
|
"verificationMethod": "did:web:dc.1edtech.org:wellspring2022:wellspring-portal:org:7ad80b28-4f3f-414d-85ec-6c0684344e5c#key-0",
|
||||||
|
"proofValue": "zYQ6iszNf2qCqisuWvk1AkerTTp69RiofNWWzWp4s5TJwzBfFgieBSA5Knyjco6crJbPkJ1mvM1hzA2HLYfU8w8C"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user