From 84a7f82c5f5d08505927509986aaa163981b4e55 Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:24:24 -0800 Subject: [PATCH 1/2] Add support for remote key material --- .../inspect/vc/probe/EmbeddedProofProbe.java | 193 +++++++++++------- 1 file changed, 115 insertions(+), 78 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index 66ca945..aa13de7 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -2,6 +2,7 @@ package org.oneedtech.inspect.vc.probe; import java.io.StringReader; import java.net.URI; +import java.util.Optional; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; @@ -9,6 +10,9 @@ import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; +import com.apicatalog.jsonld.StringUtils; +import com.apicatalog.jsonld.document.Document; +import com.apicatalog.jsonld.loader.DocumentLoaderOptions; import com.apicatalog.ld.DocumentError; import com.apicatalog.multibase.Multibase; import com.apicatalog.multicodec.Multicodec; @@ -16,38 +20,44 @@ import com.apicatalog.multicodec.Multicodec.Codec; import com.apicatalog.vc.processor.StatusVerifier; import com.danubetech.verifiablecredentials.VerifiableCredential; +import foundation.identity.jsonld.ConfigurableDocumentLoader; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; +import jakarta.json.JsonObject; +import jakarta.json.JsonStructure; /** * A Probe that verifies a credential's embedded proof. + * * @author mgylling */ public class EmbeddedProofProbe extends Probe { - + public EmbeddedProofProbe() { super(ID); } - - + /* - * Using verifiable-credentials-java (https://github.com/danubetech/verifiable-credentials-java) + * Using verifiable-credentials-java + * (https://github.com/danubetech/verifiable-credentials-java) */ - @Override - public ReportItems run(Credential crd, RunContext ctx) throws Exception { - - //TODO check that proof is Ed25519 - issue error if not ("type": "Ed25519Signature2020", - //TODO check value "proofPurpose": "assertionMethod", if not error - + @Override + public ReportItems run(Credential crd, RunContext ctx) throws Exception { + + // TODO check that proof is Ed25519 - issue error if not ("type": + // "Ed25519Signature2020", + // TODO check value "proofPurpose": "assertionMethod", if not error + VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); - vc.setDocumentLoader(new CachingDocumentLoader()); - + vc.setDocumentLoader(new CachingDocumentLoader()); + URI method = vc.getLdProof().getVerificationMethod(); // The verification method must dereference to an Ed25519VerificationKey2020. // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key // from the Ed25519VerificationKey2020 (32 bytes). - String publicKeyMultibase = ""; + String publicKeyMultibase; + String controller; // Formats accepted: // @@ -55,25 +65,51 @@ public class EmbeddedProofProbe extends Probe { // did:key:[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 + // 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(); - - if (method.getFragment() != null) { - publicKeyMultibase = method.getFragment(); - } else { - if (method.getScheme().equals("did")) { - if (method.getSchemeSpecificPart().startsWith("key:")) { - publicKeyMultibase = method.getSchemeSpecificPart().substring(4); - } 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); - } - } + + if (method.getFragment() != null) { + publicKeyMultibase = method.getFragment(); + } else { + if (method.getScheme().equals("did")) { + if (method.getSchemeSpecificPart().startsWith("key:")) { + publicKeyMultibase = method.getSchemeSpecificPart().substring(4); + } else { + return error("Unknown verification method: " + method, ctx); + } + } else if (method.getScheme().equals("http") || method.getScheme().equals("https")) { + ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader(); + documentLoader.setEnableHttp(true); + documentLoader.setEnableHttps(true); + + // The verificationMethod URI must resolve to a Ed25519VerificationKey2020 as described here: + // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 + Document keyDocument = documentLoader.loadDocument(method, new DocumentLoaderOptions()); + Optional keyStructure = keyDocument.getJsonContent(); + if (keyStructure.isEmpty()) { + return error("Key document not found at " + method, ctx); + } + // First look for a plain key object + controller = keyStructure.get().asJsonObject().getString("controller"); + if (StringUtils.isBlank(controller)) { + // Then look for a controller document (e.g. DID 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 // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 @@ -89,14 +125,15 @@ public class EmbeddedProofProbe extends Probe { // Extract the publicKey bytes from the Multicodec byte[] publicKey = Multicodec.decode(Codec.Ed25519PublicKey, publicKeyMulticodec); - - Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey); - - //TODO find out whether we also should check that controller matches issuer ID: + + 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. - + // if [publicKeyMultibase] -- don't check issuer ID. Maybe we should warn about + // this syntax. + try { boolean verify = verifier.verify(vc); if (!verify) { @@ -104,59 +141,59 @@ public class EmbeddedProofProbe extends Probe { } } catch (Exception e) { return fatal("Embedded proof verification failed:" + e.getMessage(), 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). + * 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); -// } - + + // /* + // * 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 - } + // noop + } } - - public static final String ID = EmbeddedProofProbe.class.getSimpleName(); + + public static final String ID = EmbeddedProofProbe.class.getSimpleName(); } From d620e9d32371e5e30e6bbe5da26f8e6b49020257 Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Thu, 10 Nov 2022 17:10:53 -0800 Subject: [PATCH 2/2] Code cleanup --- .../inspect/vc/probe/EmbeddedProofProbe.java | 109 ++++++------------ 1 file changed, 33 insertions(+), 76 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index aa13de7..2a3242e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -13,14 +13,13 @@ import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import com.apicatalog.jsonld.StringUtils; import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.loader.DocumentLoaderOptions; -import com.apicatalog.ld.DocumentError; import com.apicatalog.multibase.Multibase; import com.apicatalog.multicodec.Multicodec; import com.apicatalog.multicodec.Multicodec.Codec; -import com.apicatalog.vc.processor.StatusVerifier; import com.danubetech.verifiablecredentials.VerifiableCredential; import foundation.identity.jsonld.ConfigurableDocumentLoader; +import info.weboftrust.ldsignatures.LdProof; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; import jakarta.json.JsonObject; import jakarta.json.JsonStructure; @@ -37,44 +36,50 @@ public class EmbeddedProofProbe extends Probe { } /* - * Using verifiable-credentials-java + * Using verifiable-credentials-java from Danubetech * (https://github.com/danubetech/verifiable-credentials-java) */ @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { - - // TODO check that proof is Ed25519 - issue error if not ("type": - // "Ed25519Signature2020", - // TODO check value "proofPurpose": "assertionMethod", if not error + + // TODO: What there are multiple proofs? VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); vc.setDocumentLoader(new CachingDocumentLoader()); - 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. // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key // from the Ed25519VerificationKey2020 (32 bytes). String publicKeyMultibase; - String controller; + String controller = null; // Formats accepted: // // [controller]#[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] - // 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(); if (method.getFragment() != null) { publicKeyMultibase = method.getFragment(); + controller = method.toString().substring(0, method.toString().indexOf("#")); } else { if (method.getScheme().equals("did")) { if (method.getSchemeSpecificPart().startsWith("key:")) { @@ -83,21 +88,22 @@ public class EmbeddedProofProbe extends Probe { return error("Unknown verification method: " + method, 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); - // The verificationMethod URI must resolve to a Ed25519VerificationKey2020 as described here: - // https://w3c-ccg.github.io/di-eddsa-2020/#ed25519verificationkey2020 Document keyDocument = documentLoader.loadDocument(method, new DocumentLoaderOptions()); Optional keyStructure = keyDocument.getJsonContent(); if (keyStructure.isEmpty()) { return error("Key document not found at " + method, ctx); } - // First look for a plain key object + + // 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) + // 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()) { @@ -120,7 +126,13 @@ public class EmbeddedProofProbe extends Probe { return error("Verification method does not contain an Ed25519 public key", ctx); } } 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 @@ -128,72 +140,17 @@ public class EmbeddedProofProbe extends Probe { 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 { boolean verify = verifier.verify(vc); if (!verify) { return error("Embedded proof verification failed.", ctx); } } catch (Exception e) { - return fatal("Embedded proof verification failed:" + e.getMessage(), ctx); + return fatal("Embedded proof verification failed: " + e.getMessage(), 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(); }