Merge pull request #76 from imsglc/feature/did_web_support

Added did:web verification method
This commit is contained in:
Xavi Aracil 2023-05-17 17:50:07 +02:00 committed by GitHub
commit c808749cc4
5 changed files with 168 additions and 14 deletions

View File

@ -2,6 +2,8 @@ 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;
@ -20,8 +22,11 @@ import com.apicatalog.multicodec.Multicodec.Codec;
import info.weboftrust.ldsignatures.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
/**
* A Probe that verifies a credential's embedded proof.
@ -41,15 +46,17 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
@Override
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) {
return error("The verifiable credential is missing a proof.", ctx);
}
// 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(
proof -> proof.isType("Ed25519Signature2020") && proof.getProofPurpose().equals("assertionMethod"))
.findFirst();
if (!selectedProof.isPresent()) {
@ -75,8 +82,10 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
//
// [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]
// http/s://[location of a controller document with a 'verificationMethod' with
// a Ed25519VerificationKey2020]
String publicKeyMultibase;
String controller = null;
@ -92,12 +101,98 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
} 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
// 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 {
return error("Unknown verification method: " + method, ctx);
}
} else if (method.getScheme().equals("http") || method.getScheme().equals("https")) {
try {
Document keyDocument = credentiaHolder.getCredential().getDocumentLoader().loadDocument(method, new DocumentLoaderOptions());
Document keyDocument = credentialHolder.getCredential().getDocumentLoader().loadDocument(method,
new DocumentLoaderOptions());
Optional<JsonStructure> keyStructure = keyDocument.getJsonContent();
if (keyStructure.isEmpty()) {
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
controller = keyStructure.get().asJsonObject().getString("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
JsonObject keyVerificationMethod = keyStructure.get().asJsonObject()
.getJsonObject("verificationMethod");
@ -140,8 +236,9 @@ public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
}
if (controller != null) {
if (!controller.equals(credentiaHolder.getCredential().getIssuer().toString())) {
return error("Key controller does not match issuer: " + credentiaHolder.getCredential().getIssuer(), ctx);
if (!controller.equals(credentialHolder.getCredential().getIssuer().toString())) {
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);
try {
boolean verify = verifier.verify(credentiaHolder.getCredential(), proof);
boolean verify = verifier.verify(credentialHolder.getCredential(), proof);
if (!verify) {
return error("Embedded proof verification failed.", ctx);
}

View File

@ -44,9 +44,18 @@ public class OB30Tests {
}
@Test
void testSimpleDidMethodJsonValid() {
void testSimpleDidKeyMethodJsonValid() {
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);
assertValid(report);
});

View File

@ -11,7 +11,8 @@ public class Samples {
public static final class JSON {
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_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_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);

View File

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