diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 8f8d07a..02d532f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -30,6 +30,8 @@ import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.util.spec.Specification; +import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; @@ -89,16 +91,17 @@ public class OB30Inspector extends VCInspector { //we expect the above to place a generated object in the context Credential crd = ctx.getGeneratedObject(Credential.ID); - - //TODO check context IRIs? the schema doesnt do this - + //TODO new check: that subject @id or IdentityObject is available (at least one is the req) + + //context and type properties + Credential.Type type = Type.OpenBadgeCredential; + for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { + probeCount++; + accumulator.add(probe.run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } - //type property - probeCount++; - accumulator.add(new TypePropertyProbe(OpenBadgeCredential).run(crd.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - //canonical schema and inline schemata SchemaKey schema = crd.getSchemaKey().orElseThrow(); for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java new file mode 100644 index 0000000..53143af --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -0,0 +1,63 @@ +package org.oneedtech.inspect.vc.probe; + +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; + +import java.util.List; +import java.util.Map; + +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.Credential; +import org.oneedtech.inspect.vc.util.JsonNodeUtil; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.collect.ImmutableMap; + +/** + * A Probe that verifies a credential's context property. + * + * @author mgylling + */ +public class ContextPropertyProbe extends Probe { + private final Credential.Type type; + + public ContextPropertyProbe(Credential.Type type) { + super(ID); + this.type = checkNotNull(type); + } + + @Override + public ReportItems run(JsonNode root, RunContext ctx) throws Exception { + + ArrayNode contextNode = (ArrayNode) root.get("@context"); + if (contextNode == null) { + return fatal("No @context property", ctx); + } + + List expected = values.get(type); + if(expected == null) { + return fatal(type.name() + " not recognized", ctx); + } + + List given = JsonNodeUtil.asStringList(contextNode); + int pos = 0; + for(String uri : expected) { + if((given.size() < pos+1) || !given.get(pos).equals(uri)) { + return error("missing required @context uri " + uri + " at position " + (pos+1), ctx); + } + pos++; + } + + return success(ctx); + } + + private final static Map> values = new ImmutableMap.Builder>() + .put(Credential.Type.OpenBadgeCredential, + List.of("https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json")) //TODO will change (https://purl.imsglobal.org/spec/ob/v3p0/context/ob_v3p0.jsonld) + .build(); + + public static final String ID = ContextPropertyProbe.class.getSimpleName(); +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 6b843e6..4471d18 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -10,6 +10,7 @@ import org.oneedtech.inspect.core.Inspector.Behavior; import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.test.PrintHelper; +import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; @@ -109,6 +110,17 @@ public class OB30Tests { }); } + @Test + void testSimpleJsonContextError() { + //removed one of the reqd context uris + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ERR_CONTEXT.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertHasProbeID(report, ContextPropertyProbe.ID, true); + }); + } + @Test void testSimpleJsonSchemaError() throws Exception { //issuer removed diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 0da143e..5ba1f27 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -17,6 +17,7 @@ public class Samples { public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-err-expired.json", false); public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false); public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false); + public final static Sample SIMPLE_JSON_ERR_CONTEXT = new Sample("ob30/simple-err-context.json", false); } public static final class PNG { public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true); diff --git a/inspector-vc/src/test/resources/ob30/simple-err-context.json b/inspector-vc/src/test/resources/ob30/simple-err-context.json new file mode 100644 index 0000000..5258f76 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-err-context.json @@ -0,0 +1,35 @@ +{ + "@context": [ + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file