Merge pull request #48 from imsglc/add-http-key-support

Add support for remote key material to inspector-vc
This commit is contained in:
Xavi Aracil 2022-11-15 10:05:02 +01:00 committed by GitHub
commit 347f44e804

View File

@ -2,24 +2,30 @@ package org.oneedtech.inspect.vc.probe;
import java.io.StringReader; import java.io.StringReader;
import java.net.URI; import java.net.URI;
import java.util.Optional;
import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential;
import com.apicatalog.ld.DocumentError; import com.apicatalog.jsonld.StringUtils;
import com.apicatalog.jsonld.document.Document;
import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.apicatalog.multibase.Multibase; import com.apicatalog.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 com.apicatalog.vc.processor.StatusVerifier;
import com.danubetech.verifiablecredentials.VerifiableCredential; import com.danubetech.verifiablecredentials.VerifiableCredential;
import foundation.identity.jsonld.ConfigurableDocumentLoader; import foundation.identity.jsonld.ConfigurableDocumentLoader;
import info.weboftrust.ldsignatures.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
import jakarta.json.JsonObject;
import jakarta.json.JsonStructure;
/** /**
* A Probe that verifies a credential's embedded proof. * A Probe that verifies a credential's embedded proof.
*
* @author mgylling * @author mgylling
*/ */
public class EmbeddedProofProbe extends Probe<Credential> { public class EmbeddedProofProbe extends Probe<Credential> {
@ -28,54 +34,92 @@ public class EmbeddedProofProbe extends Probe<Credential> {
super(ID); super(ID);
} }
/* /*
* Using verifiable-credentials-java (https://github.com/danubetech/verifiable-credentials-java) * Using verifiable-credentials-java from Danubetech
* (https://github.com/danubetech/verifiable-credentials-java)
*/ */
@Override @Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception { public ReportItems run(Credential crd, RunContext ctx) throws Exception {
//TODO check that proof is Ed25519 - issue error if not ("type": "Ed25519Signature2020", // TODO: What there are multiple proofs?
//TODO check value "proofPurpose": "assertionMethod", if not error
VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString()));
ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader(); ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader();
documentLoader.setEnableHttp(true); documentLoader.setEnableHttp(true);
documentLoader.setEnableHttps(true); documentLoader.setEnableHttps(true);
vc.setDocumentLoader(new CachingDocumentLoader());
vc.setDocumentLoader(documentLoader); vc.setDocumentLoader(documentLoader);
URI method = vc.getLdProof().getVerificationMethod(); URI method = vc.getLdProof().getVerificationMethod();
LdProof proof = vc.getLdProof();
if (proof == null) {
return error("The verifiable credential is missing a proof.", ctx);
}
if (!proof.isType("Ed25519Signature2020")) {
return error("Unknown proof type: " + proof.getType(), ctx);
}
if (!proof.getProofPurpose().equals("assertionMethod")) {
return error("Invalid proof purpose: " + proof.getProofPurpose(), ctx);
}
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).
String publicKeyMultibase = ""; String publicKeyMultibase;
String controller = null;
// Formats accepted: // Formats accepted:
// //
// [controller]#[publicKeyMultibase] // [controller]#[publicKeyMultibase]
// did:key:[publicKeyMultibase] // did:key:[publicKeyMultibase]
// http/s://[location of a Ed25519VerificationKey2020 document]
// http/s://[location of a controller document with a 'verificationMethod' with a Ed25519VerificationKey2020]
// [publicKeyMultibase] // [publicKeyMultibase]
// TODO fourth format that we don't support yet: a URL that returns a Ed25519VerificationKey2020
// if starts with http and does not have hashcode, try fetch and see if returns Ed25519VerificationKey2020
// property is publicKeyMultibase
publicKeyMultibase = method.toString(); publicKeyMultibase = method.toString();
if (method.getFragment() != null) { if (method.getFragment() != null) {
publicKeyMultibase = method.getFragment(); publicKeyMultibase = method.getFragment();
} else { controller = method.toString().substring(0, method.toString().indexOf("#"));
if (method.getScheme().equals("did")) { } else {
if (method.getSchemeSpecificPart().startsWith("key:")) { if (method.getScheme().equals("did")) {
publicKeyMultibase = method.getSchemeSpecificPart().substring(4); if (method.getSchemeSpecificPart().startsWith("key:")) {
} else { publicKeyMultibase = method.getSchemeSpecificPart().substring(4);
return error("Unknown verification method: " + method, ctx); } else {
} return error("Unknown verification method: " + method, ctx);
} else if (method.getScheme().equals("http") || method.getScheme().equals("https")) { }
return error("Cannot parse http verification key yet", ctx); } else if (method.getScheme().equals("http") || method.getScheme().equals("https")) {
} // TODO: Can we use proof.getDocumentLoader()?
} ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader();
documentLoader.setEnableHttp(true);
documentLoader.setEnableHttps(true);
Document keyDocument = documentLoader.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
controller = keyStructure.get().asJsonObject().getString("controller");
if (StringUtils.isBlank(controller)) {
// Then look for a controller document (e.g. DID Document) with a 'verificationMethod'
// that is a Ed25519VerificationKey2020 document
JsonObject keyVerificationMethod = keyStructure.get().asJsonObject()
.getJsonObject("verificationMethod");
if (keyVerificationMethod.isEmpty()) {
return error("Cannot parse key document from " + method, ctx);
}
controller = keyVerificationMethod.getString("controller");
publicKeyMultibase = keyVerificationMethod.getString("publicKeyMultibase");
} else {
publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase");
}
}
}
// 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
@ -86,7 +130,13 @@ public class EmbeddedProofProbe extends Probe<Credential> {
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("Verification method is invalid: " + e.getMessage(), ctx); return fatal("Invalid public key: " + e.getMessage(), ctx);
}
if (controller != null) {
if (!controller.equals(vc.getIssuer().toString())) {
return error("Key controller does not match issuer: " + vc.getIssuer(), ctx);
}
} }
// Extract the publicKey bytes from the Multicodec // Extract the publicKey bytes from the Multicodec
@ -94,71 +144,17 @@ public class EmbeddedProofProbe extends Probe<Credential> {
Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey); Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey);
//TODO find out whether we also should check that controller matches issuer ID:
// if [controller]#[publicKeyMultibase] format - check [controller] segment
// if did:key:[publicKeyMultibase] format: issuer ID must match the entire URI
// if [publicKeyMultibase] -- don't check issuer ID. Maybe we should warn about this syntax.
try { try {
boolean verify = verifier.verify(vc); boolean verify = verifier.verify(vc);
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);
} }
/*
* Note: if using com.apicatalog Iron, we get a generic VC verifier that
* will test other stuff than the Proof. So sometimes it may be that
* Iron internally retests something that we're already testing out in the
* Inspector class (e.g. expiration). But use this for now -- and remember
* that this probe is only run if the given credential has internal proof
* (aka is not a jwt).
*/
// /*
// * Using iron-verifiable-credentials (https://github.com/filip26/iron-verifiable-credentials)
// */
// @Override
// public ReportItems run(Credential crd, RunContext ctx) throws Exception {
// JsonDocument jsonDoc = JsonDocument.of(new StringReader(crd.getJson().toString()));
// JsonObject json = jsonDoc.getJsonContent().get().asJsonObject();
// try {
// Vc.verify(json)
// .loader(new CachingDocumentLoader())
// .useBundledContexts(false) //we control the cache in the loader
// .statusVerifier(new IronNoopStatusVerifier())
// //.domain(...)
// //.didResolver(...)
// .isValid();
// } catch (DocumentError e) {
// return error(e.getType() + " " + e.getSubject(), ctx);
// } catch (VerificationError e) {
// //System.err.println(e.getCode() + " (ProofVerifierProbe)");
// if(e.getCode() == Code.Internal) {
// return exception(e.getMessage(), ctx.getResource());
// } else if(e.getCode().equals(Code.Expired)) {
// //handled by other probe
// } else {
// return fatal(e.getCode().name() + " " + e.getMessage(), ctx);
// }
//
// }
// return success(ctx);
// }
private static final class IronNoopStatusVerifier implements StatusVerifier {
@Override
public void verify(Status status) throws DocumentError, VerifyError {
// noop
}
}
public static final String ID = EmbeddedProofProbe.class.getSimpleName(); public static final String ID = EmbeddedProofProbe.class.getSimpleName();
} }