diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml
new file mode 100644
index 0000000..8545db0
--- /dev/null
+++ b/inspector-vc/pom.xml
@@ -0,0 +1,85 @@
+
+ 4.0.0
+
+ org.1edtech
+ inspector
+ 0.9.2
+
+ inspector-vc
+
+
+ org.1edtech
+ inspector-core
+
+
+ com.auth0
+ auth0
+ 1.42.0
+
+
+ com.auth0
+ jwks-rsa
+ 0.21.1
+
+
+ com.auth0
+ java-jwt
+ 3.19.2
+
+
+
+
+ com.danubetech
+ verifiable-credentials-java
+
+ 1.1-SNAPSHOT
+
+
+
+
+ com.danubetech
+ key-formats-java
+ 1.6-SNAPSHOT
+
+
+
+
+ com.apicatalog
+ iron-verifiable-credentials-jre8
+ 0.7.0
+
+
+
+
+ com.apicatalog
+ titanium-json-ld
+ 1.3.1
+
+
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+
+
+
+
+ org.glassfish
+ jakarta.json
+ 2.0.1
+
+
+
+
+
+ danubetech-maven-public
+ https://repo.danubetech.com/repository/maven-public/
+
+
+
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java
new file mode 100644
index 0000000..1b3c501
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java
@@ -0,0 +1,133 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.util.code.Defensives.*;
+import static org.oneedtech.inspect.util.resource.ResourceType.*;
+import static org.oneedtech.inspect.vc.Credential.Type.*;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+import org.oneedtech.inspect.schema.Catalog;
+import org.oneedtech.inspect.schema.SchemaKey;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * A wrapper object for a verifiable credential. This contains e.g. the origin resource
+ * and the extracted JSON data plus any other stuff Probes need.
+ * @author mgylling
+ */
+public class Credential extends GeneratedObject {
+ final Resource resource;
+ final JsonNode jsonData;
+ final Credential.Type credentialType;
+ final String jwt;
+
+ public Credential(Resource resource, JsonNode data, String jwt) {
+ super(ID, GeneratedObject.Type.INTERNAL);
+ this.resource = checkNotNull(resource);
+ this.jsonData = checkNotNull(data);
+ this.jwt = jwt; //may be null
+
+ checkTrue(RECOGNIZED_PAYLOAD_TYPES.contains(resource.getType()));
+
+ ArrayNode typeNode = (ArrayNode)jsonData.get("type");
+ this.credentialType = Credential.Type.valueOf(typeNode);
+ }
+
+ public Credential(Resource resource, JsonNode data) {
+ this(resource, data, null);
+ }
+
+ public Resource getResource() {
+ return resource;
+ }
+
+ public JsonNode getJson() {
+ return jsonData;
+ }
+
+ public Credential.Type getCredentialType() {
+ return credentialType;
+ }
+
+ public Optional getJwt() {
+ return Optional.ofNullable(jwt);
+ }
+
+ public ProofType getProofType() {
+ return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL;
+ }
+
+
+ private static final Map schemas = new ImmutableMap.Builder()
+ .put(AchievementCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON)
+ .put(ClrCredential, Catalog.CLR_20_CLRCREDENTIAL_JSON)
+ .put(VerifiablePresentation, Catalog.CLR_20_CLRCREDENTIAL_JSON)
+ .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON)
+ .build();
+
+ /**
+ * Get the canonical schema for this credential if such exists.
+ */
+ public Optional getSchemaKey() {
+ return Optional.ofNullable(schemas.get(credentialType));
+ }
+
+ public enum Type {
+ AchievementCredential,
+ OpenBadgeCredential, //treated as an alias of AchievementCredential
+ ClrCredential,
+ EndorsementCredential,
+ VerifiablePresentation,
+ VerifiableCredential, //this is an underspecifier in our context
+ Unknown;
+
+ public static Credential.Type valueOf (ArrayNode typeArray) {
+ if(typeArray != null) {
+ Iterator iter = typeArray.iterator();
+ while(iter.hasNext()) {
+ String value = iter.next().asText();
+ if(value.equals("AchievementCredential") || value.equals("OpenBadgeCredential")) {
+ return AchievementCredential;
+ } else if(value.equals("ClrCredential")) {
+ return ClrCredential;
+ } else if(value.equals("VerifiablePresentation")) {
+ return VerifiablePresentation;
+ } else if(value.equals("EndorsementCredential")) {
+ return EndorsementCredential;
+ }
+ }
+ }
+ return Unknown;
+ }
+ }
+
+ public enum ProofType {
+ EXTERNAL,
+ EMBEDDED
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("resource", resource.getID())
+ .add("resourceType", resource.getType())
+ .add("credentialType", credentialType)
+ .add("json", jsonData)
+ .toString();
+ }
+
+ public static final String ID = Credential.class.getCanonicalName();
+ public static final List RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT);
+ public static final String CREDENTIAL_KEY = "CREDENTIAL_KEY";
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
new file mode 100644
index 0000000..2eb643c
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
@@ -0,0 +1,141 @@
+package org.oneedtech.inspect.vc;
+
+import static java.lang.Boolean.TRUE;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.*;
+import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
+import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.oneedtech.inspect.core.SubInspector;
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.core.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.util.resource.context.ResourceContext;
+import org.oneedtech.inspect.vc.Credential.Type;
+import org.oneedtech.inspect.vc.probe.ContextPropertyProbe;
+import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe;
+import org.oneedtech.inspect.vc.probe.ExpirationProbe;
+import org.oneedtech.inspect.vc.probe.ExternalProofProbe;
+import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceProbe;
+import org.oneedtech.inspect.vc.probe.RevocationListProbe;
+import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * An inspector for EndorsementCredential objects.
+ * @author mgylling
+ */
+public class EndorsementInspector extends VCInspector implements SubInspector {
+
+ protected > EndorsementInspector(B builder) {
+ super(builder);
+ }
+
+ @Override
+ public Report run(Resource resource, Map parentObjects) {
+
+ /*
+ * The resource param is the top-level credential that embeds the endorsement, we
+ * expect parentObjects to provide a pointer to the JsonNode we should check.
+ *
+ * The parent inspector is responsible to decode away possible jwt-ness, so that
+ * what we get here is a verbatim json node.
+ *
+ */
+
+ Credential endorsement = (Credential) checkNotNull(parentObjects.get(CREDENTIAL_KEY));
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(JACKSON_OBJECTMAPPER, mapper)
+ .put(JSONPATH_EVALUATOR, jsonPath)
+ .build();
+
+ List accumulator = new ArrayList<>();
+ int probeCount = 0;
+ try {
+
+ //context and type properties
+ Credential.Type type = Type.EndorsementCredential;
+ for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
+ probeCount++;
+ accumulator.add(probe.run(endorsement.getJson(), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ //inline schema (parent inspector has already validated against canonical)
+ accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx));
+
+ //signatures, proofs
+ probeCount++;
+ if(endorsement.getProofType() == EXTERNAL){
+ //The credential originally contained in a JWT, validate the jwt and external proof.
+ accumulator.add(new ExternalProofProbe().run(endorsement, ctx));
+ } else {
+ //The credential not contained in a jwt, must have an internal proof.
+ accumulator.add(new EmbeddedProofProbe().run(endorsement, ctx));
+
+ }
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //check refresh service if we are not already refreshed (check just like in external CLR)
+ probeCount++;
+ if(resource.getContext().get(REFRESHED) != TRUE) {
+ Optional newID = checkRefreshService(endorsement, ctx);
+ if(newID.isPresent()) {
+ //TODO resource.type
+ return this.run(
+ new UriResource(new URI(newID.get()))
+ .setContext(new ResourceContext(REFRESHED, TRUE)));
+ }
+ }
+
+ //revocation, expiration and issuance
+ for(Probe probe : List.of(new RevocationListProbe(),
+ new ExpirationProbe(), new IssuanceProbe())) {
+ probeCount++;
+ accumulator.add(probe.run(endorsement, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ @Override
+ public Report run(R resource) {
+ throw new IllegalStateException("must use #run(resource, map)");
+ }
+
+ public static class Builder extends VCInspector.Builder {
+ @SuppressWarnings("unchecked")
+ @Override
+ public EndorsementInspector build() {
+ return new EndorsementInspector(this);
+ }
+ }
+
+}
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
new file mode 100644
index 0000000..96e4236
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java
@@ -0,0 +1,232 @@
+package org.oneedtech.inspect.vc;
+
+import static java.lang.Boolean.TRUE;
+import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN;
+import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
+import static org.oneedtech.inspect.util.code.Defensives.*;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
+import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL;
+import static org.oneedtech.inspect.vc.payload.PayloadParser.fromJwt;
+import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.oneedtech.inspect.core.SubInspector;
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.RunContext.Key;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe;
+import org.oneedtech.inspect.core.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.schema.JsonSchemaCache;
+import org.oneedtech.inspect.schema.SchemaKey;
+import org.oneedtech.inspect.util.code.Defensives;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.resource.Resource;
+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.CredentialSubjectProbe;
+import org.oneedtech.inspect.vc.probe.ExpirationProbe;
+import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceProbe;
+import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe;
+import org.oneedtech.inspect.vc.probe.RevocationListProbe;
+import org.oneedtech.inspect.vc.probe.ExternalProofProbe;
+import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * A verifier for Open Badges 3.0.
+ * @author mgylling
+ */
+public class OB30Inspector extends VCInspector implements SubInspector {
+ protected final List> userProbes;
+
+ protected OB30Inspector(OB30Inspector.Builder builder) {
+ super(builder);
+ this.userProbes = ImmutableList.copyOf(builder.probes);
+ }
+
+ //https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit#
+ //https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#verificaton-and-validation
+
+ /*
+ * This inspector supports both standalone openbadge verification, as well as verification of
+ * AchievementCredentials embedded in e.g. CLR.
+ *
+ * When verifying a standalone AchievementCredential, call the run(Resource) method. When verifying
+ * an embedded AchievementCredential, call the run(Resource, Map) method.
+ */
+
+ @Override
+ public Report run(Resource resource) {
+ super.check(resource); //TODO because URIs, this should be a fetch and cache
+
+ if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) {
+ JsonSchemaCache.reset();
+ CachingDocumentLoader.reset();
+ }
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(resource)
+ .put(Key.JACKSON_OBJECTMAPPER, mapper)
+ .put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .build();
+
+ List accumulator = new ArrayList<>();
+ int probeCount = 0;
+
+ try {
+ //detect type (png, svg, json, jwt) and extract json data
+ probeCount++;
+ accumulator.add(new CredentialParseProbe().run(resource, ctx));
+ if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
+
+ //we expect the above to place a generated object in the context
+ Credential ob = ctx.getGeneratedObject(Credential.ID);
+
+ //call the subinspector method of this
+ Report subReport = this.run(resource, Map.of(Credential.CREDENTIAL_KEY, ob));
+ probeCount += subReport.getSummary().getTotalRun();
+ accumulator.add(subReport);
+
+ //finally, run any user-added probes
+ for(Probe probe : userProbes) {
+ probeCount++;
+ accumulator.add(probe.run(ob, ctx));
+ }
+
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ @Override
+ public Report run(Resource resource, Map parentObjects) {
+
+ Credential ob = checkNotNull((Credential)parentObjects.get(CREDENTIAL_KEY));
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(resource)
+ .put(Key.JACKSON_OBJECTMAPPER, mapper)
+ .put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .build();
+
+ List accumulator = new ArrayList<>();
+ int probeCount = 0;
+
+ try {
+
+ //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(ob.getJson(), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ //canonical schema and inline schemata
+ SchemaKey schema = ob.getSchemaKey().orElseThrow();
+ for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) {
+ probeCount++;
+ accumulator.add(probe.run(ob.getJson(), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ //credentialSubject
+ probeCount++;
+ accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx));
+
+ //signatures, proofs
+ probeCount++;
+ if(ob.getProofType() == EXTERNAL){
+ //The credential originally contained in a JWT, validate the jwt and external proof.
+ accumulator.add(new ExternalProofProbe().run(ob, ctx));
+ } else {
+ //The credential not contained in a jwt, must have an internal proof.
+ accumulator.add(new EmbeddedProofProbe().run(ob, ctx));
+ }
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //check refresh service if we are not already refreshed
+ probeCount++;
+ if(resource.getContext().get(REFRESHED) != TRUE) {
+ Optional newID = checkRefreshService(ob, ctx);
+ if(newID.isPresent()) {
+ return this.run(
+ new UriResource(new URI(newID.get()))
+ .setContext(new ResourceContext(REFRESHED, TRUE)));
+ }
+ }
+
+ //revocation, expiration and issuance
+ for(Probe probe : List.of(new RevocationListProbe(),
+ new ExpirationProbe(), new IssuanceProbe())) {
+ probeCount++;
+ accumulator.add(probe.run(ob, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ //embedded endorsements
+ EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build();
+
+ List endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath);
+ for(JsonNode node : endorsements) {
+ probeCount++;
+ Credential endorsement = new Credential(resource, node);
+ accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
+ }
+
+ //embedded jwt endorsements
+ endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath);
+ for(JsonNode node : endorsements) {
+ probeCount++;
+ String jwt = node.asText();
+ JsonNode vcNode = fromJwt(jwt, ctx);
+ Credential endorsement = new Credential(resource, vcNode, jwt);
+ accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
+ }
+
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+
+ }
+
+ public static class Builder extends VCInspector.Builder {
+ @SuppressWarnings("unchecked")
+ @Override
+ public OB30Inspector build() {
+ set(Specification.OB30);
+ set(ResourceType.OPENBADGE);
+ return new OB30Inspector(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
new file mode 100644
index 0000000..a4a90dc
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
@@ -0,0 +1,79 @@
+package org.oneedtech.inspect.vc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.oneedtech.inspect.core.Inspector;
+import org.oneedtech.inspect.core.probe.Outcome;
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Abstract base for verifiable credentials inspectors/verifiers.
+ * @author mgylling
+ */
+public abstract class VCInspector extends Inspector {
+
+ protected > VCInspector(B builder) {
+ super(builder);
+ }
+
+ protected Report abort(RunContext ctx, List accumulator, int probeCount) {
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ protected boolean broken(List accumulator) {
+ return broken(accumulator, false);
+ }
+
+ protected boolean broken(List accumulator, boolean force) {
+ if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) {
+ return false;
+ }
+ for(ReportItems items : accumulator) {
+ if(items.contains(Outcome.FATAL, Outcome.EXCEPTION)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the AchievementCredential or EndorsementCredential has a “refreshService” property and the type of the
+ * RefreshService object is “1EdTechCredentialRefresh”, you should fetch the refreshed credential from the URL
+ * provided, then start the verification process over using the response as input. If the request fails,
+ * the credential is invalid.
+ */
+ protected Optional checkRefreshService(Credential crd, RunContext ctx) {
+ JsonNode refreshServiceNode = crd.getJson().get("refreshService");
+ if(refreshServiceNode != null) {
+ JsonNode serviceTypeNode = refreshServiceNode.get("type");
+ if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) {
+ JsonNode serviceURINode = refreshServiceNode.get("id");
+ if(serviceURINode != null) {
+ return Optional.of(serviceURINode.asText());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ protected static final String REFRESHED = "is.refreshed.credential";
+
+ public abstract static class Builder> extends Inspector.Builder {
+ final List> probes;
+
+ public Builder() {
+ super();
+ this.probes = new ArrayList<>();
+ }
+
+ public VCInspector.Builder add(Probe probe) {
+ probes.add(probe);
+ return this;
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java
new file mode 100644
index 0000000..6552312
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java
@@ -0,0 +1,32 @@
+package org.oneedtech.inspect.vc.payload;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A credential extractor for JSON files.
+ * @author mgylling
+ */
+public final class JsonParser extends PayloadParser {
+
+ @Override
+ public boolean supports(ResourceType type) {
+ return type == ResourceType.JSON;
+ }
+
+ @Override
+ public Credential parse(Resource resource, RunContext ctx) throws Exception {
+ checkTrue(resource.getType() == ResourceType.JSON);
+ String json = resource.asByteSource().asCharSource(UTF_8).read();
+ JsonNode node = fromString(json, ctx);
+ return new Credential(resource, node);
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java
new file mode 100644
index 0000000..3a946b1
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java
@@ -0,0 +1,32 @@
+package org.oneedtech.inspect.vc.payload;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A credential extractor for JWT files.
+ * @author mgylling
+ */
+public final class JwtParser extends PayloadParser {
+
+ @Override
+ public boolean supports(ResourceType type) {
+ return type == ResourceType.JWT;
+ }
+
+ @Override
+ public Credential parse(Resource resource, RunContext ctx) throws Exception {
+ checkTrue(resource.getType() == ResourceType.JWT);
+ String jwt = resource.asByteSource().asCharSource(UTF_8).read();
+ JsonNode node = fromJwt(jwt, ctx);
+ return new Credential(resource, node, jwt);
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java
new file mode 100644
index 0000000..41ccb2c
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java
@@ -0,0 +1,52 @@
+package org.oneedtech.inspect.vc.payload;
+
+import java.util.Base64;
+import java.util.List;
+import java.util.Base64.Decoder;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+
+/**
+ * Abstract base for extracting Credential instances from payloads.
+ * @author mgylling
+ */
+public abstract class PayloadParser {
+
+ public abstract boolean supports(ResourceType type);
+
+ public abstract Credential parse(Resource source, RunContext ctx) throws Exception;
+
+ protected static JsonNode fromString(String json, RunContext context) throws Exception {
+ return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json);
+ }
+
+ /**
+ * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof
+ * @return The decoded JSON String
+ */
+ public static JsonNode fromJwt(String jwt, RunContext context) throws Exception {
+ List parts = Splitter.on('.').splitToList(jwt);
+ if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
+
+ final Decoder decoder = Base64.getUrlDecoder();
+ /*
+ * For this step we are only deserializing the stored badge out of the payload.
+ * The entire jwt is stored separately for signature verification later.
+ */
+ String jwtPayload = new String(decoder.decode(parts.get(1)));
+
+ //Deserialize and fetch the 'vc' node from the object
+ JsonNode outerPayload = fromString(jwtPayload, context);
+ JsonNode vcNode = outerPayload.get("vc");
+
+ return vcNode;
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java
new file mode 100644
index 0000000..a2f0cc3
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java
@@ -0,0 +1,25 @@
+package org.oneedtech.inspect.vc.payload;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+
+import java.util.List;
+
+import org.oneedtech.inspect.util.resource.Resource;
+
+/**
+ * A factory to create PayloadParser instances for various resource types.
+ * @author mgylling
+ */
+public class PayloadParserFactory {
+ private static final Iterable parsers = List.of(
+ new PngParser(), new SvgParser(),
+ new JsonParser(), new JwtParser());
+
+ public static PayloadParser of(Resource resource) {
+ checkNotNull(resource.getType());
+ for(PayloadParser cex : parsers) {
+ if(cex.supports(resource.getType())) return cex;
+ }
+ throw new IllegalArgumentException();
+ }
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java
new file mode 100644
index 0000000..f521e47
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java
@@ -0,0 +1,102 @@
+package org.oneedtech.inspect.vc.payload;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+
+import java.io.InputStream;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.metadata.IIOMetadata;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.vc.Credential;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A credential extractor for PNG images.
+ * @author mgylling
+ */
+public final class PngParser extends PayloadParser {
+
+ @Override
+ public boolean supports(ResourceType type) {
+ return type == ResourceType.PNG;
+ }
+
+ @Override
+ public Credential parse(Resource resource, RunContext ctx) throws Exception {
+
+ checkTrue(resource.getType() == ResourceType.PNG);
+
+ try(InputStream is = resource.asByteSource().openStream()) {
+
+ ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next();
+ imageReader.setInput(ImageIO.createImageInputStream(is), true);
+ IIOMetadata metadata = imageReader.getImageMetadata(0);
+
+ String vcString = null;
+ String jwtString = null;
+ String formatSearch = null;
+ JsonNode vcNode = null;
+
+ String[] names = metadata.getMetadataFormatNames();
+ int length = names.length;
+ for (int i = 0; i < length; i++) {
+ //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0")
+ formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]));
+ if(formatSearch != null) {
+ vcString = formatSearch;
+ break;
+ }
+ }
+
+ if(vcString == null) {
+ throw new IllegalArgumentException("No credential inside PNG");
+ }
+
+ vcString = vcString.trim();
+ if(vcString.charAt(0) != '{'){
+ //This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification.
+ jwtString = vcString;
+ vcNode = fromJwt(vcString, ctx);
+ }
+ else {
+ vcNode = fromString(vcString, ctx);
+ }
+
+ return new Credential(resource, vcNode, jwtString);
+ }
+ }
+
+ private String getOpenBadgeCredentialNodeText(Node node){
+ NamedNodeMap attributes = node.getAttributes();
+
+ //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one.
+ Node keyword = attributes.getNamedItem("keyword");
+ if(keyword != null && keyword.getNodeValue().equals("openbadgecredential")){
+ Node textAttribute = attributes.getNamedItem("text");
+ if(textAttribute != null) {
+ return textAttribute.getNodeValue();
+ }
+ }
+
+ //iterate over all children depth first and search for the credential node.
+ Node child = node.getFirstChild();
+ while (child != null) {
+ String nodeValue = getOpenBadgeCredentialNodeText(child);
+ if(nodeValue != null) {
+ return nodeValue;
+ }
+ child = child.getNextSibling();
+ }
+
+ //Return null if we haven't found anything at this recursive depth.
+ return null;
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java
new file mode 100644
index 0000000..f600bc9
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java
@@ -0,0 +1,79 @@
+package org.oneedtech.inspect.vc.payload;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+
+import java.io.InputStream;
+
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.events.Attribute;
+import javax.xml.stream.events.Characters;
+import javax.xml.stream.events.XMLEvent;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.util.code.Defensives;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.util.xml.XMLInputFactoryCache;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A credential extractor for SVG documents.
+ * @author mgylling
+ */
+public final class SvgParser extends PayloadParser {
+
+ @Override
+ public boolean supports(ResourceType type) {
+ return type == ResourceType.SVG;
+ }
+
+ @Override
+ public Credential parse(Resource resource, RunContext ctx) throws Exception {
+
+ checkTrue(resource.getType() == ResourceType.SVG);
+
+ try(InputStream is = resource.asByteSource().openStream()) {
+ XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is);
+ while(reader.hasNext()) {
+ XMLEvent ev = reader.nextEvent();
+ if(isEndElem(ev, OB_CRED_ELEM)) break;
+ if(isStartElem(ev, OB_CRED_ELEM)) {
+ Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR);
+ if(verifyAttr != null) {
+ String jwt = verifyAttr.getValue();
+ JsonNode node = fromJwt(jwt, ctx);
+ return new Credential(resource, node, jwt);
+ } else {
+ while(reader.hasNext()) {
+ ev = reader.nextEvent();
+ if(isEndElem(ev, OB_CRED_ELEM)) break;
+ if(ev.getEventType() == XMLEvent.CHARACTERS) {
+ Characters chars = ev.asCharacters();
+ if(!chars.isWhiteSpace()) {
+ JsonNode node = fromString(chars.getData(), ctx);
+ return new Credential(resource, node);
+ }
+ }
+ }
+ }
+ }
+ } //while(reader.hasNext()) {
+ }
+ throw new IllegalArgumentException("No credential inside SVG");
+
+ }
+
+ private boolean isEndElem(XMLEvent ev, QName name) {
+ return ev.isEndElement() && ev.asEndElement().getName().equals(name);
+ }
+
+ private boolean isStartElem(XMLEvent ev, QName name) {
+ return ev.isStartElement() && ev.asStartElement().getName().equals(name);
+ }
+
+ private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential");
+ private static final QName OB_CRED_VERIFY_ATTR = new QName("verify");
+}
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..5747336
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java
@@ -0,0 +1,76 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.vc.Credential.Type.*;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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 notRun("No @context property", ctx);
+ }
+
+ List expected = values.get(values.keySet()
+ .stream()
+ .filter(s->s.contains(type))
+ .findFirst()
+ .orElseThrow(()-> new IllegalArgumentException(type.name() + " not recognized")));
+
+ 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, List> values = new ImmutableMap.Builder, List>()
+ .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential),
+ List.of("https://www.w3.org/2018/credentials/v1",
+ //"https://imsglobal.github.io/openbadges-specification/context.json")) //dev legacy
+ "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
+ .put(Set.of(ClrCredential),
+ List.of("https://www.w3.org/2018/credentials/v1",
+// "https://dc.imsglobal.org/draft/clr/v2p0/context", //dev legacy
+// "https://imsglobal.github.io/openbadges-specification/context.json")) //dev legacy
+ "https://purl.imsglobal.org/spec/clr/v2p0/context.json",
+ "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
+
+ .build();
+
+ public static final String ID = ContextPropertyProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java
new file mode 100644
index 0000000..02a549b
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java
@@ -0,0 +1,54 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.util.Optional;
+
+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.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.util.resource.detect.TypeDetector;
+import org.oneedtech.inspect.vc.Credential;
+import org.oneedtech.inspect.vc.payload.PayloadParserFactory;
+
+/**
+ * A probe that verifies that the incoming credential resource is of a recognized payload type
+ * and if so extracts and stores the VC json data (a 'Credential' instance)
+ * in the RunContext.
+ * @author mgylling
+ */
+public class CredentialParseProbe extends Probe {
+
+ @Override
+ public ReportItems run(Resource resource, RunContext context) throws Exception {
+
+ try {
+
+ //TODO if .detect reads from a URIResource twice. Cache the resource on first call.
+
+ Optional type = Optional.ofNullable(resource.getType());
+ if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) {
+ type = TypeDetector.detect(resource, true);
+ if(type.isEmpty()) {
+ //TODO if URI fetch, TypeDetector likely to fail
+ System.err.println("typedetector fail: extend behavior here");
+ return fatal("Could not detect credential payload type", context);
+ } else {
+ resource.setType(type.get());
+ }
+ }
+
+ if(!Credential.RECOGNIZED_PAYLOAD_TYPES.contains(type.get())) {
+ return fatal("Payload type not supported: " + type.get().getName(), context);
+ }
+
+ Credential crd = PayloadParserFactory.of(resource).parse(resource, context);
+ context.addGeneratedObject(crd);
+ return success(this, context);
+
+ } catch (Exception e) {
+ return fatal("Error while parsing credential: " + e.getMessage(), context);
+ }
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java
new file mode 100644
index 0000000..2932563
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java
@@ -0,0 +1,42 @@
+package org.oneedtech.inspect.vc.probe;
+
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.ReportItems;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+/**
+ * A Probe that checks credential subject specifics not capturable by schemata.
+ *
+ * @author mgylling
+ */
+public class CredentialSubjectProbe extends Probe {
+
+ public CredentialSubjectProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+
+ JsonNode subject = root.get("credentialSubject");
+ if(subject == null) return notRun("no credentialSubject node found", ctx); //error reported by schema
+
+ /*
+ * Check that we have either .id or .identifier populated
+ */
+ JsonNode id = root.get("id");
+ if (id != null && id.textValue().strip().length() > 0) return success(ctx);
+
+ JsonNode identifier = root.get("identifier");
+ if(identifier != null && identifier instanceof ArrayNode
+ && ((ArrayNode)identifier).size() > 0) return success(ctx);
+
+ return error("no id in credentialSubject", ctx);
+
+ }
+
+ public static final String ID = CredentialSubjectProbe.class.getSimpleName();
+}
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
new file mode 100644
index 0000000..554a0cf
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
@@ -0,0 +1,159 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.io.StringReader;
+import java.net.URI;
+
+import org.bouncycastle.util.Arrays;
+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.CachingDocumentLoader;
+
+import com.apicatalog.ld.DocumentError;
+import com.apicatalog.multibase.Multibase;
+import com.apicatalog.vc.processor.StatusVerifier;
+import com.danubetech.verifiablecredentials.VerifiableCredential;
+
+import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
+
+/**
+ * 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)
+ */
+ @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());
+
+ 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 = "";
+
+ // Formats accepted:
+ //
+ // [controller]#[publicKeyMultibase]
+ // 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
+ // property is publicKeyMultibase
+
+ if (method.toString().contains("#")) {
+ publicKeyMultibase = method.getFragment();
+ } else {
+ if (method.toString().startsWith("did")) {
+ String didScheme = method.getSchemeSpecificPart();
+ if (didScheme.startsWith("key:")) {
+ publicKeyMultibase = didScheme.substring(4);
+ } else {
+ return error("Unknown verification method: " + method.toString(), ctx);
+ }
+ } else {
+ publicKeyMultibase = method.toString();
+ }
+ }
+
+ // Decode the Multibase to Multicodec and check that it is an Ed25519 public key
+ byte[] publicKeyMulticodec;
+ try {
+ publicKeyMulticodec = Multibase.decode(publicKeyMultibase);
+ if (publicKeyMulticodec[0] != -19 || publicKeyMulticodec[1] != 1) {
+ return error("Verification method does not contain an Ed25519 public key", ctx);
+ }
+ } catch (Exception e) {
+ return error("Verification method is invalid: " + e.getMessage(), ctx);
+ }
+
+ // Extract the publicKey bytes from the Multicodec
+ byte[] publicKey = Arrays.copyOfRange(publicKeyMulticodec, 2, publicKeyMulticodec.length);
+
+ 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 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();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java
new file mode 100644
index 0000000..fcebd0c
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java
@@ -0,0 +1,43 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.time.ZonedDateTime;
+
+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 com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A Probe that verifies a credential's expiration status
+ * @author mgylling
+ */
+public class ExpirationProbe extends Probe {
+
+ public ExpirationProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ /*
+ * If the AchievementCredential or EndorsementCredential has an “expirationDate” property
+ * and the expiration date is prior to the current date, the credential has expired.
+ */
+ JsonNode node = crd.getJson().get("expirationDate");
+ if(node != null) {
+ try {
+ ZonedDateTime expirationDate = ZonedDateTime.parse(node.textValue());
+ if (ZonedDateTime.now().isAfter(expirationDate)) {
+ return fatal("The credential has expired (expiration date was " + node.asText() + ").", ctx);
+ }
+ } catch (Exception e) {
+ return exception("Error while checking expirationDate: " + e.getMessage(), ctx.getResource());
+ }
+ }
+ return success(ctx);
+ }
+
+ public static final String ID = ExpirationProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java
new file mode 100644
index 0000000..b4bccb0
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java
@@ -0,0 +1,151 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.RSAPublicKeySpec;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.List;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+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 com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.AlgorithmMismatchException;
+import com.auth0.jwt.exceptions.InvalidClaimException;
+import com.auth0.jwt.exceptions.SignatureVerificationException;
+import com.auth0.jwt.exceptions.TokenExpiredException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+
+/**
+ * A Probe that verifies credential external proof (jwt)
+ * @author mlyon
+ */
+public class ExternalProofProbe extends Probe {
+
+ public ExternalProofProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ try {
+ verifySignature(crd, ctx);
+ } catch (Exception e) {
+ return fatal("Error verifying jwt signature: " + e.getMessage(), ctx);
+ }
+ return success(ctx);
+ }
+
+ private void verifySignature(Credential crd, RunContext ctx) throws Exception {
+ checkTrue(crd.getJwt().isPresent(), "no jwt supplied");
+ checkTrue(crd.getJwt().get().length() > 0, "no jwt supplied");
+
+ DecodedJWT decodedJwt = null;
+ String jwt = crd.getJwt().get();
+
+ List parts = Splitter.on('.').splitToList(jwt);
+ if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
+
+ final Decoder decoder = Base64.getUrlDecoder();
+ String joseHeader = new String(decoder.decode(parts.get(0)));
+
+ ObjectMapper mapper = ((ObjectMapper)ctx.get(RunContext.Key.JACKSON_OBJECTMAPPER));
+ JsonNode headerObj = mapper.readTree(joseHeader);
+
+ //MUST be "RS256"
+ JsonNode alg = headerObj.get("alg");
+ if(alg == null || !alg.textValue().equals("RS256")) { throw new Exception("alg must be present and must be 'RS256'"); }
+
+ //TODO: decoded jwt will check timestamps, but shall we explicitly break these out?
+
+ //Option 1, fetch directly from header
+ JsonNode jwk = headerObj.get("jwk");
+
+ //Option 2, fetch from a hosting url
+ JsonNode kid = headerObj.get("kid");
+
+ if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); }
+ if(kid != null){
+ //Load jwk JsonNode from url and do the rest the same below.
+ //TODO Consider additional testing.
+ String kidUrl = kid.textValue();
+ String jwkResponse = fetchJwk(kidUrl);
+ if(jwkResponse == null) { throw new Exception("Unable to retrieve jwk value from url specified in kid."); }
+
+ jwk = mapper.readTree(jwkResponse);
+ }
+
+ //Clean up may be required. Currently need to cleanse extra double quoting.
+ String modulusString = jwk.get("n").textValue();
+ String exponentString = jwk.get("e").textValue();
+
+ BigInteger modulus = new BigInteger(1, decoder.decode(modulusString));
+ BigInteger exponent = new BigInteger(1, decoder.decode(exponentString));
+
+ PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
+
+ Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null);
+ JWTVerifier verifier = JWT.require(algorithm)
+ .build(); //Reusable verifier instance
+ try {
+ decodedJwt = verifier.verify(jwt);
+ }
+ catch(SignatureVerificationException ex){
+ throw new Exception("JWT Invalid signature", ex);
+ }
+ catch(AlgorithmMismatchException ex){
+ throw new Exception("JWT Algorithm mismatch", ex);
+ }
+ catch(TokenExpiredException ex){
+ throw new Exception("JWT Token expired", ex);
+ }
+ catch(InvalidClaimException ex){
+ throw new Exception("JWT, one or more claims are invalid", ex);
+ }
+ }
+
+ private String fetchJwk(String fetchUrl){
+ String responseString = null;
+
+ try {
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet httpGet = new HttpGet(fetchUrl);
+
+ CloseableHttpResponse response = client.execute(httpGet);
+
+ if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ HttpEntity entity = response.getEntity();
+ responseString = EntityUtils.toString(entity, "UTF-8");
+ }
+
+ client.close();
+ }
+ catch(Exception ex){
+ responseString = null;
+ }
+
+ return responseString;
+ }
+
+ public static final String ID = ExternalProofProbe.class.getSimpleName();
+
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
new file mode 100644
index 0000000..7f3f86e
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
@@ -0,0 +1,74 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.schema.SchemaKey;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+/**
+ * Detect inline schemas in a credential and run them.
+ * @author mgylling
+ */
+public class InlineJsonSchemaProbe extends Probe {
+ private static final Set types = Set.of("1EdTechJsonSchemaValidator2019");
+ private SchemaKey skip;
+
+ public InlineJsonSchemaProbe() {
+ super(ID);
+ }
+
+ public InlineJsonSchemaProbe(SchemaKey skip) {
+ super(ID);
+ this.skip = skip;
+ }
+
+ @Override
+ public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+ List accumulator = new ArrayList<>();
+ Set ioErrors = new HashSet<>();
+
+ //note - we don't get deep nested ones in e.g. EndorsementCredential
+ JsonNode credentialSchemaNode = root.get("credentialSchema");
+ if(credentialSchemaNode == null) return success(ctx);
+
+ ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast
+
+ for(JsonNode schemaNode : schemas) {
+ JsonNode typeNode = schemaNode.get("type");
+ if(typeNode == null || !types.contains(typeNode.asText())) continue;
+ JsonNode idNode = schemaNode.get("id");
+ if(idNode == null) continue;
+ String id = idNode.asText().strip();
+ if(ioErrors.contains(id)) continue;
+ if(equals(skip, id)) continue;
+ try {
+ accumulator.add(new JsonSchemaProbe(id).run(root, ctx));
+ } catch (Exception e) {
+ if(!ioErrors.contains(id)) {
+ ioErrors.add(id);
+ accumulator.add(error("Could not read schema resource " + id, ctx));
+ }
+ }
+ }
+
+ return new ReportItems(accumulator);
+ }
+
+ private boolean equals(SchemaKey key, String id) {
+ if(key == null) return false;
+ return key.getCanonicalURI().equals(id);
+ }
+
+ public static final String ID = InlineJsonSchemaProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java
new file mode 100644
index 0000000..04c5d6b
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java
@@ -0,0 +1,43 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.time.ZonedDateTime;
+
+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 com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * A Probe that verifies a credential's issuance status
+ * @author mgylling
+ */
+public class IssuanceProbe extends Probe {
+
+ public IssuanceProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ /*
+ * If the AchievementCredential or EndorsementCredential “issuanceDate”
+ * property after the current date, the credential is not yet valid.
+ */
+ JsonNode node = crd.getJson().get("issuanceDate");
+ if(node != null) {
+ try {
+ ZonedDateTime issuanceDate = ZonedDateTime.parse(node.textValue());
+ if (issuanceDate.isAfter(ZonedDateTime.now())) {
+ return fatal("The credential is not yet issued (issuance date is " + node.asText() + ").", ctx);
+ }
+ } catch (Exception e) {
+ return exception("Error while checking issuanceDate: " + e.getMessage(), ctx.getResource());
+ }
+ }
+ return success(ctx);
+ }
+
+ public static final String ID = IssuanceProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java
new file mode 100644
index 0000000..ca11b0f
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java
@@ -0,0 +1,79 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPPER;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+
+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.ObjectMapper;
+
+/**
+ * A Probe that verifies a credential's revocation status.
+ * @author mgylling
+ */
+public class RevocationListProbe extends Probe {
+
+ public RevocationListProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+
+ /*
+ * If the AchievementCredential or EndorsementCredential has a “credentialStatus” property
+ * and the type of the CredentialStatus object is “1EdTechRevocationList”, fetch the
+ * credential status from the URL provided. If the request is unsuccessful,
+ * report a warning, not an error.
+ */
+
+ JsonNode credentialStatus = crd.getJson().get("credentialStatus");
+ if(credentialStatus != null) {
+ JsonNode type = credentialStatus.get("type");
+ if(type != null && type.asText().strip().equals("1EdTechRevocationList")) {
+ JsonNode listID = credentialStatus.get("id");
+ if(listID != null) {
+ try {
+ URL url = new URI(listID.asText().strip()).toURL();
+ try (InputStream is = url.openStream()) {
+ JsonNode revocList = ((ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER)).readTree(is.readAllBytes());
+
+ /* To check if a credential has been revoked, the verifier issues a GET request
+ * to the URL of the issuer's 1EdTech Revocation List Status Method. If the
+ * credential's id is in the list of revokedCredentials and the value of
+ * revoked is true or ommitted, the issuer has revoked the credential. */
+
+ JsonNode crdID = crd.getJson().get("id"); //TODO these != checks sb removed (trigger warning)
+ if(crdID != null) {
+ List list = JsonNodeUtil.asNodeList(revocList.get("revokedCredentials"));
+ if(list != null) {
+ for(JsonNode item : list) {
+ JsonNode revID = item.get("id");
+ JsonNode revoked = item.get("revoked");
+ if(revID != null && revID.equals(crdID) && (revoked == null || revoked.asBoolean())) {
+ return fatal("Credential has been revoked", ctx);
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ return warning("Error when fetching credentialStatus resource " + e.getMessage(), ctx);
+ }
+ }
+ }
+ }
+ return success(ctx);
+ }
+
+ public static final String ID = RevocationListProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java
new file mode 100644
index 0000000..804380e
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java
@@ -0,0 +1,64 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+
+import java.util.List;
+
+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.Credential.Type;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+/**
+ * A Probe that verifies a credential's type property.
+ *
+ * @author mgylling
+ */
+public class TypePropertyProbe extends Probe {
+ private final Credential.Type expected;
+
+ public TypePropertyProbe(Credential.Type expected) {
+ super(ID);
+ this.expected = checkNotNull(expected);
+ }
+
+ @Override
+ public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+
+ ArrayNode typeNode = (ArrayNode) root.get("type");
+ if (typeNode == null)
+ return fatal("No type property", ctx);
+
+ List values = JsonNodeUtil.asStringList(typeNode);
+
+ if (!values.contains("VerifiableCredential")) {
+ return fatal("The type property does not contain the entry 'VerifiableCredential'", ctx);
+ }
+
+ if (expected == Credential.Type.OpenBadgeCredential) {
+ if (!values.contains("OpenBadgeCredential") && !values.contains("AchievementCredential")) {
+ return fatal("The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", ctx);
+ }
+ } else if (expected == Credential.Type.ClrCredential) {
+ if (!values.contains("ClrCredential")) {
+ return fatal("The type property does not contain the entry 'ClrCredential'", ctx);
+ }
+ } else if (expected == Credential.Type.EndorsementCredential) {
+ if (!values.contains("EndorsementCredential")) {
+ return fatal("The type property does not contain the entry 'EndorsementCredential'", ctx);
+ }
+ } else {
+ // TODO implement
+ throw new IllegalStateException();
+ }
+
+ return success(ctx);
+ }
+
+ public static final String ID = TypePropertyProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
new file mode 100644
index 0000000..b656419
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
@@ -0,0 +1,78 @@
+package org.oneedtech.inspect.vc.util;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.time.Duration;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.oneedtech.inspect.util.code.Tuple;
+
+import com.apicatalog.jsonld.JsonLdError;
+import com.apicatalog.jsonld.JsonLdErrorCode;
+import com.apicatalog.jsonld.document.Document;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Resources;
+
+/**
+ * A com.apicatalog DocumentLoader with a threadsafe static cache.
+ *
+ * @author mgylling
+ */
+public class CachingDocumentLoader implements DocumentLoader {
+
+ @Override
+ public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError {
+ Tuple tpl = new Tuple<>(url.toASCIIString(), options);
+ try {
+ return documentCache.get(tpl);
+ } catch (Exception e) {
+ logger.error("documentCache not able to load {}", url);
+ throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage());
+ }
+ }
+
+ static final ImmutableMap bundled = ImmutableMap.builder()
+ .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("contexts/clr-v2p0.json"))
+ .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("contexts/ob-v3p0.json"))
+ .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3x.jsonld"))
+ .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld"))
+ .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld"))
+ .put("https://w3id.org/security/suites/ed25519-2020/v1",Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld"))
+ .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld"))
+ .put("https://w3id.org/security/v1", Resources.getResource("contexts/security-v1.jsonld"))
+ .put("https://w3id.org/security/v2", Resources.getResource("contexts/security-v2.jsonld"))
+ .put("https://w3id.org/security/v3", Resources.getResource("contexts/security-v3-unstable.jsonld"))
+ .put("https://w3id.org/security/bbs/v1", Resources.getResource("contexts/security-bbs-v1.jsonld"))
+ .put("https://w3id.org/security/suites/secp256k1-2019/v1", Resources.getResource("contexts/suites-secp256k1-2019.jsonld"))
+ .put("https://w3id.org/security/suites/ed25519-2018/v1", Resources.getResource("contexts/suites-ed25519-2018.jsonld"))
+ .put("https://w3id.org/security/suites/x25519-2019/v1", Resources.getResource("contexts/suites-x25519-2019.jsonld"))
+ .put("https://w3id.org/security/suites/jws-2020/v1", Resources.getResource("contexts/suites-jws-2020.jsonld"))
+
+ .build();
+
+ static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder()
+ .initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24))
+ .build(new CacheLoader, Document>() {
+ public Document load(final Tuple id) throws Exception {
+ try (InputStream is = bundled.keySet().contains(id.t1)
+ ? bundled.get(id.t1).openStream()
+ : new URI(id.t1).toURL().openStream();) {
+ return JsonDocument.of(is);
+ }
+ }
+ });
+
+ public static void reset() {
+ documentCache.invalidateAll();
+ }
+
+ private static final Logger logger = LogManager.getLogger();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java
new file mode 100644
index 0000000..aaf6083
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java
@@ -0,0 +1,58 @@
+package org.oneedtech.inspect.vc.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+/**
+ * Json node utilities.
+ * @author mgylling
+ */
+public class JsonNodeUtil {
+
+ public static List asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) {
+ List list = new ArrayList<>();
+ ArrayNode array = evaluator.eval(jsonPath, root);
+ for(JsonNode node : array) {
+ if(!(node instanceof ArrayNode)) {
+ list.add(node);
+ } else {
+ ArrayNode values = (ArrayNode) node;
+ for(JsonNode value : values) {
+ list.add(value);
+ }
+ }
+ }
+ return list;
+ }
+
+ public static List asStringList(JsonNode node) {
+ if(!(node instanceof ArrayNode)) {
+ return List.of(node.asText());
+ } else {
+ ArrayNode arrayNode = (ArrayNode)node;
+ return StreamSupport
+ .stream(arrayNode.spliterator(), false)
+ .map(n->n.asText().strip())
+ .collect(Collectors.toList());
+ }
+ }
+
+ public static List asNodeList(JsonNode node) {
+ if(node == null) return null;
+ if(!(node instanceof ArrayNode)) {
+ return List.of(node);
+ } else {
+ ArrayNode arrayNode = (ArrayNode)node;
+ return StreamSupport
+ .stream(arrayNode.spliterator(), false)
+ .collect(Collectors.toList());
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld b/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld
new file mode 100644
index 0000000..425199c
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld
@@ -0,0 +1,69 @@
+{
+ "@context": [
+ {
+ "@version": 1.1
+ },
+ "https://www.w3.org/ns/odrl.jsonld",
+ {
+ "ex": "https://example.org/examples#",
+ "schema": "http://schema.org/",
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "3rdPartyCorrelation": "ex:3rdPartyCorrelation",
+ "AllVerifiers": "ex:AllVerifiers",
+ "Archival": "ex:Archival",
+ "BachelorDegree": "ex:BachelorDegree",
+ "Child": "ex:Child",
+ "CLCredentialDefinition2019": "ex:CLCredentialDefinition2019",
+ "CLSignature2019": "ex:CLSignature2019",
+ "IssuerPolicy": "ex:IssuerPolicy",
+ "HolderPolicy": "ex:HolderPolicy",
+ "Mother": "ex:Mother",
+ "RelationshipCredential": "ex:RelationshipCredential",
+ "UniversityDegreeCredential": "ex:UniversityDegreeCredential",
+ "AlumniCredential": "ex:AlumniCredential",
+ "DisputeCredential": "ex:DisputeCredential",
+ "PrescriptionCredential": "ex:PrescriptionCredential",
+ "ZkpExampleSchema2018": "ex:ZkpExampleSchema2018",
+ "issuerData": "ex:issuerData",
+ "attributes": "ex:attributes",
+ "signature": "ex:signature",
+ "signatureCorrectnessProof": "ex:signatureCorrectnessProof",
+ "primaryProof": "ex:primaryProof",
+ "nonRevocationProof": "ex:nonRevocationProof",
+ "alumniOf": {
+ "@id": "schema:alumniOf",
+ "@type": "rdf:HTML"
+ },
+ "child": {
+ "@id": "ex:child",
+ "@type": "@id"
+ },
+ "degree": "ex:degree",
+ "degreeType": "ex:degreeType",
+ "degreeSchool": "ex:degreeSchool",
+ "college": "ex:college",
+ "name": {
+ "@id": "schema:name",
+ "@type": "rdf:HTML"
+ },
+ "givenName": "schema:givenName",
+ "familyName": "schema:familyName",
+ "parent": {
+ "@id": "ex:parent",
+ "@type": "@id"
+ },
+ "referenceId": "ex:referenceId",
+ "documentPresence": "ex:documentPresence",
+ "evidenceDocument": "ex:evidenceDocument",
+ "spouse": "schema:spouse",
+ "subjectPresence": "ex:subjectPresence",
+ "verifier": {
+ "@id": "ex:verifier",
+ "@type": "@id"
+ },
+ "currentStatus": "ex:currentStatus",
+ "statusReason": "ex:statusReason",
+ "prescription": "ex:prescription"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld b/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld
new file mode 100644
index 0000000..837104f
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld
@@ -0,0 +1,315 @@
+{
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "VerifiableCredential": {
+ "@id": "https://www.w3.org/2018/credentials#VerifiableCredential",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "cred": "https://www.w3.org/2018/credentials#",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "credentialSchema": {
+ "@id": "cred:credentialSchema",
+ "@type": "@id",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "cred": "https://www.w3.org/2018/credentials#",
+ "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018"
+ }
+ },
+ "credentialStatus": {
+ "@id": "cred:credentialStatus",
+ "@type": "@id"
+ },
+ "credentialSubject": {
+ "@id": "cred:credentialSubject",
+ "@type": "@id"
+ },
+ "evidence": {
+ "@id": "cred:evidence",
+ "@type": "@id"
+ },
+ "expirationDate": {
+ "@id": "cred:expirationDate",
+ "@type": "xsd:dateTime"
+ },
+ "holder": {
+ "@id": "cred:holder",
+ "@type": "@id"
+ },
+ "issued": {
+ "@id": "cred:issued",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "cred:issuer",
+ "@type": "@id"
+ },
+ "issuanceDate": {
+ "@id": "cred:issuanceDate",
+ "@type": "xsd:dateTime"
+ },
+ "proof": {
+ "@id": "sec:proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "refreshService": {
+ "@id": "cred:refreshService",
+ "@type": "@id",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "cred": "https://www.w3.org/2018/credentials#",
+ "ManualRefreshService2018": "cred:ManualRefreshService2018"
+ }
+ },
+ "termsOfUse": {
+ "@id": "cred:termsOfUse",
+ "@type": "@id"
+ },
+ "validFrom": {
+ "@id": "cred:validFrom",
+ "@type": "xsd:dateTime"
+ },
+ "validUntil": {
+ "@id": "cred:validUntil",
+ "@type": "xsd:dateTime"
+ }
+ }
+ },
+ "VerifiablePresentation": {
+ "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "cred": "https://www.w3.org/2018/credentials#",
+ "sec": "https://w3id.org/security#",
+ "holder": {
+ "@id": "cred:holder",
+ "@type": "@id"
+ },
+ "proof": {
+ "@id": "sec:proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "verifiableCredential": {
+ "@id": "cred:verifiableCredential",
+ "@type": "@id",
+ "@container": "@graph"
+ }
+ }
+ },
+ "EcdsaSecp256k1Signature2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "challenge": "sec:challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "xsd:dateTime"
+ },
+ "domain": "sec:domain",
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "jws": "sec:jws",
+ "nonce": "sec:nonce",
+ "proofPurpose": {
+ "@id": "sec:proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "assertionMethod": {
+ "@id": "sec:assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "sec:authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "sec:proofValue",
+ "verificationMethod": {
+ "@id": "sec:verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "EcdsaSecp256r1Signature2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "challenge": "sec:challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "xsd:dateTime"
+ },
+ "domain": "sec:domain",
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "jws": "sec:jws",
+ "nonce": "sec:nonce",
+ "proofPurpose": {
+ "@id": "sec:proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "assertionMethod": {
+ "@id": "sec:assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "sec:authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "sec:proofValue",
+ "verificationMethod": {
+ "@id": "sec:verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "Ed25519Signature2018": {
+ "@id": "https://w3id.org/security#Ed25519Signature2018",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "challenge": "sec:challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "xsd:dateTime"
+ },
+ "domain": "sec:domain",
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "jws": "sec:jws",
+ "nonce": "sec:nonce",
+ "proofPurpose": {
+ "@id": "sec:proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "assertionMethod": {
+ "@id": "sec:assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "sec:authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "sec:proofValue",
+ "verificationMethod": {
+ "@id": "sec:verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "RsaSignature2018": {
+ "@id": "https://w3id.org/security#RsaSignature2018",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "challenge": "sec:challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "xsd:dateTime"
+ },
+ "domain": "sec:domain",
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "jws": "sec:jws",
+ "nonce": "sec:nonce",
+ "proofPurpose": {
+ "@id": "sec:proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "assertionMethod": {
+ "@id": "sec:assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "sec:authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "sec:proofValue",
+ "verificationMethod": {
+ "@id": "sec:verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/clr-v2p0.json b/inspector-vc/src/main/resources/contexts/clr-v2p0.json
new file mode 100644
index 0000000..d1e1b93
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/clr-v2p0.json
@@ -0,0 +1,53 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "ClrCredential": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#ClrCredential",
+ "@context": {
+ "id": "@id",
+ "type": "@type"
+ }
+ },
+ "ClrSubject": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#ClrSubject",
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "cred": "https://www.w3.org/2018/credentials#",
+ "obi": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#",
+ "achievement": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#achievement",
+ "@type": "obi:Achievement",
+ "@container": "@set"
+ },
+ "association": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#association",
+ "@type": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#Association",
+ "@container": "@set"
+ },
+ "verifiableCredential": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#verifiableCredential",
+ "@type": "cred:verifiableCredential",
+ "@container": "@set"
+ }
+ }
+ },
+ "Association": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#Association",
+ "@context": {
+ "associationType": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#AssociationType"
+ },
+ "sourceId": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#sourceId",
+ "@type": "xsd:anyURI"
+ },
+ "targetId": {
+ "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#targetId",
+ "@type": "xsd:anyURI"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/did-v1.jsonld b/inspector-vc/src/main/resources/contexts/did-v1.jsonld
new file mode 100644
index 0000000..55ab11c
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/did-v1.jsonld
@@ -0,0 +1,58 @@
+{
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+
+ "alsoKnownAs": {
+ "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs",
+ "@type": "@id"
+ },
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "service": {
+ "@id": "https://www.w3.org/ns/did#service",
+ "@type": "@id",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "serviceEndpoint": {
+ "@id": "https://www.w3.org/ns/did#serviceEndpoint",
+ "@type": "@id"
+ }
+ }
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/ob-v3p0.json b/inspector-vc/src/main/resources/contexts/ob-v3p0.json
new file mode 100644
index 0000000..8e133c8
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/ob-v3p0.json
@@ -0,0 +1,440 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+
+ "xsd": "https://www.w3.org/2001/XMLSchema#",
+
+ "OpenBadgeCredential": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#OpenBadgeCredential"
+ },
+ "Achievement": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Achievement",
+ "@context": {
+ "achievementType": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#achievementType",
+ "@type": "xsd:string"
+ },
+ "alignment": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#alignment",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Alignment",
+ "@container": "@set"
+ },
+ "creator": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile"
+ },
+ "creditsAvailable": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#creditsAvailable",
+ "@type": "xsd:float"
+ },
+ "criteria": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Criteria",
+ "@type": "@id"
+ },
+ "fieldOfStudy": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#fieldOfStudy",
+ "@type": "xsd:string"
+ },
+ "humanCode": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#humanCode",
+ "@type": "xsd:string"
+ },
+ "otherIdentifier": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#otherIdentifier",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry",
+ "@container": "@set"
+ },
+ "related": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#related",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Related",
+ "@container": "@set"
+ },
+ "resultDescription": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultDescription",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#ResultDescription",
+ "@container": "@set"
+ },
+ "specialization": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#specialization",
+ "@type": "xsd:string"
+ },
+ "tag": {
+ "@id": "https://schema.org/keywords",
+ "@type": "xsd:string",
+ "@container": "@set"
+ },
+ "version": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#version",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "AchievementCredential": {
+ "@id": "OpenBadgeCredential"
+ },
+ "AchievementSubject": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#AchievementSubject",
+ "@context": {
+ "achievement": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Achievement"
+ },
+ "activityEndDate": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#activityEndDate",
+ "@type": "xsd:date"
+ },
+ "activityStartDate": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#activityStartDate",
+ "@type": "xsd:date"
+ },
+ "creditsEarned": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#creditsEarned",
+ "@type": "xsd:float"
+ },
+ "identifier": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifier",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentityObject",
+ "@container": "@set"
+ },
+ "licenseNumber": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#licenseNumber",
+ "@type": "xsd:string"
+ },
+ "result": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#result",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Result",
+ "@container": "@set"
+ },
+ "role": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#role",
+ "@type": "xsd:string"
+ },
+ "source": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#source",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile"
+ },
+ "term": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#term",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Address": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Address",
+ "@context": {
+ "addressCountry": {
+ "@id": "https://schema.org/addressCountry",
+ "@type": "xsd:string"
+ },
+ "addressCountryCode": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#CountryCode",
+ "@type": "xsd:string"
+ },
+ "addressLocality": {
+ "@id": "https://schema.org/addressLocality",
+ "@type": "xsd:string"
+ },
+ "addressRegion": {
+ "@id": "https://schema.org/addressRegion",
+ "@type": "xsd:string"
+ },
+ "geo": {
+ "@id" : "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#GeoCoordinates"
+ },
+ "postOfficeBoxNumber": {
+ "@id": "https://schema.org/postOfficeBoxNumber",
+ "@type": "xsd:string"
+ },
+ "postalCode": {
+ "@id": "https://schema.org/postalCode",
+ "@type": "xsd:string"
+ },
+ "streetAddress": {
+ "@id": "https://schema.org/streetAddress",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Alignment": {
+ "@id": "https://schema.org/Alignment",
+ "@context": {
+ "targetCode": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#targetCode",
+ "@type": "xsd:string"
+ },
+ "targetDescription": {
+ "@id": "https://schema.org/targetDescription",
+ "@type": "xsd:string"
+ },
+ "targetFramework": {
+ "@id": "https://schema.org/targetFramework",
+ "@type": "xsd:string"
+ },
+ "targetName": {
+ "@id": "https://schema.org/targetName",
+ "@type": "xsd:string"
+ },
+ "targetType": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#targetType",
+ "@type": "xsd:string"
+ },
+ "targetUrl": {
+ "@id": "https://schema.org/targetUrl",
+ "@type": "xsd:anyURI"
+ }
+ }
+ },
+ "Criteria": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Criteria"
+ },
+ "EndorsementCredential": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementCredential"
+ },
+ "EndorsementSubject": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementSubject",
+ "@context": {
+ "endorsementComment": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#endorsementComment",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Evidence": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Evidence",
+ "@context": {
+ "audience": {
+ "@id": "https://schema.org/audience",
+ "@type": "xsd:string"
+ },
+ "genre": {
+ "@id": "https://schema.org/genre",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "GeoCoordinates": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#GeoCoordinates",
+ "@context": {
+ "latitude": {
+ "@id": "https://schema.org/latitude",
+ "@type": "xsd:string"
+ },
+ "longitude": {
+ "@id": "https://schema.org/longitude",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "IdentifierEntry": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry",
+ "@context": {
+ "identifier": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifier",
+ "@type": "xsd:string"
+ },
+ "identifierType": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifierType",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "IdentityObject": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentityObject",
+ "@context": {
+ "hashed": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#hashed",
+ "@type": "xsd:boolean"
+ },
+ "identityHash": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identityHash",
+ "@type": "xsd:string"
+ },
+ "identityType": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identityType",
+ "@type": "xsd:string"
+ },
+ "salt": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#salt",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Image": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Image",
+ "@context": {
+ "caption": {
+ "@id": "https://schema.org/caption",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Profile": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile",
+ "@context": {
+ "additionalName": {
+ "@id": "https://schema.org/additionalName",
+ "@type": "xsd:string"
+ },
+ "address": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Address"
+ },
+ "dateOfBirth": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#dateOfBirth",
+ "@type": "xsd:date"
+ },
+ "email": {
+ "@id": "https://schema.org/email",
+ "@type": "xsd:string"
+ },
+ "familyName": {
+ "@id": "https://schema.org/familyName",
+ "@type": "xsd:string"
+ },
+ "familyNamePrefix": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#familyNamePrefix",
+ "@type": "xsd:string"
+ },
+ "givenName": {
+ "@id": "https://schema.org/givenName",
+ "@type": "xsd:string"
+ },
+ "honorificPrefix": {
+ "@id": "https://schema.org/honorificPrefix",
+ "@type": "xsd:string"
+ },
+ "honorificSuffix": {
+ "@id": "https://schema.org/honorificSuffix",
+ "@type": "xsd:string"
+ },
+ "otherIdentifier": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#otherIdentifier",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry",
+ "@container": "@set"
+ },
+ "parentOrg": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#parentOrg",
+ "@type": "xsd:string"
+ },
+ "patronymicName": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#patronymicName",
+ "@type": "xsd:string"
+ },
+ "phone": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#PhoneNumber",
+ "@type": "xsd:string"
+ },
+ "official": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#official",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Related": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Related",
+ "@context": {
+ "version": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#version",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Result": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Result",
+ "@context": {
+ "achievedLevel": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#achievedLevel",
+ "@type": "xsd:anyURI"
+ },
+ "resultDescription": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultDescription",
+ "@type": "xsd:anyURI"
+ },
+ "status": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#status",
+ "@type": "xsd:string"
+ },
+ "value": {
+ "@id": "https://schema.org/value",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "ResultDescription": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#ResultDescription",
+ "@context": {
+ "allowedValue": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#allowedValue",
+ "@type": "xsd:string",
+ "@container": "@set"
+ },
+ "requiredLevel": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#requiredLevel",
+ "@type": "xsd:anyURI"
+ },
+ "requiredValue": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#requiredValue",
+ "@type": "xsd:string"
+ },
+ "resultType": {
+ "@id":"https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultType",
+ "@type": "xsd:string"
+ },
+ "rubricCriterionLevel": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#rubricCriterionLevel",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#RubricCriterionLevel",
+ "@container": "@set"
+ },
+ "valueMax": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#valueMax",
+ "@type": "xsd:string"
+ },
+ "valueMin": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#valueMin",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "RubricCriterionLevel": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#RubricCriterionLevel",
+ "@context": {
+ "level": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#level",
+ "@type": "xsd:string"
+ },
+ "points": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#points",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "alignment": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#alignment",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Alignment",
+ "@container": "@set"
+ },
+ "description": {
+ "@id": "https://schema.org/description",
+ "@type": "xsd:string"
+ },
+ "endorsement": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#endorsement",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementCredential",
+ "@container": "@set"
+ },
+ "image": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#image",
+ "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Image"
+ },
+ "name": {
+ "@id": "https://schema.org/name",
+ "@type": "xsd:string"
+ },
+ "narrative": {
+ "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#narrative",
+ "@type": "xsd:string"
+ },
+ "url": {
+ "@id": "https://schema.org/url",
+ "@type": "xsd:anyURI"
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/obv3x.jsonld b/inspector-vc/src/main/resources/contexts/obv3x.jsonld
new file mode 100644
index 0000000..301efa5
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/obv3x.jsonld
@@ -0,0 +1,440 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+
+ "xsd": "https://www.w3.org/2001/XMLSchema#",
+
+ "OpenBadgeCredential": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential"
+ },
+ "Achievement": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Achievement",
+ "@context": {
+ "achievementType": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#achievementType",
+ "@type": "xsd:string"
+ },
+ "alignment": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#alignment",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Alignment",
+ "@container": "@set"
+ },
+ "creator": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile"
+ },
+ "creditsAvailable": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#creditsAvailable",
+ "@type": "xsd:float"
+ },
+ "criteria": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Criteria",
+ "@type": "@id"
+ },
+ "fieldOfStudy": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#fieldOfStudy",
+ "@type": "xsd:string"
+ },
+ "humanCode": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#humanCode",
+ "@type": "xsd:string"
+ },
+ "otherIdentifier": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#otherIdentifier",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry",
+ "@container": "@set"
+ },
+ "related": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#related",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Related",
+ "@container": "@set"
+ },
+ "resultDescription": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultDescription",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#ResultDescription",
+ "@container": "@set"
+ },
+ "specialization": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#specialization",
+ "@type": "xsd:string"
+ },
+ "tag": {
+ "@id": "https://schema.org/keywords",
+ "@type": "xsd:string",
+ "@container": "@set"
+ },
+ "version": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#version",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "AchievementCredential": {
+ "@id": "OpenBadgeCredential"
+ },
+ "AchievementSubject": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#AchievementSubject",
+ "@context": {
+ "achievement": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Achievement"
+ },
+ "activityEndDate": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#activityEndDate",
+ "@type": "xsd:date"
+ },
+ "activityStartDate": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#activityStartDate",
+ "@type": "xsd:date"
+ },
+ "creditsEarned": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#creditsEarned",
+ "@type": "xsd:float"
+ },
+ "identifier": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifier",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentityObject",
+ "@container": "@set"
+ },
+ "licenseNumber": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#licenseNumber",
+ "@type": "xsd:string"
+ },
+ "result": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#result",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Result",
+ "@container": "@set"
+ },
+ "role": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#role",
+ "@type": "xsd:string"
+ },
+ "source": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#source",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile"
+ },
+ "term": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#term",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Address": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Address",
+ "@context": {
+ "addressCountry": {
+ "@id": "https://schema.org/addressCountry",
+ "@type": "xsd:string"
+ },
+ "addressCountryCode": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#CountryCode",
+ "@type": "xsd:string"
+ },
+ "addressLocality": {
+ "@id": "https://schema.org/addresLocality",
+ "@type": "xsd:string"
+ },
+ "addressRegion": {
+ "@id": "https://schema.org/addressRegion",
+ "@type": "xsd:string"
+ },
+ "geo": {
+ "@id" : "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#GeoCoordinates"
+ },
+ "postOfficeBoxNumber": {
+ "@id": "https://schema.org/postOfficeBoxNumber",
+ "@type": "xsd:string"
+ },
+ "postalCode": {
+ "@id": "https://schema.org/postalCode",
+ "@type": "xsd:string"
+ },
+ "streetAddress": {
+ "@id": "https://schema.org/streetAddress",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Alignment": {
+ "@id": "https://schema.org/Alignment",
+ "@context": {
+ "targetCode": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#targetCode",
+ "@type": "xsd:string"
+ },
+ "targetDescription": {
+ "@id": "https://schema.org/targetDescription",
+ "@type": "xsd:string"
+ },
+ "targetFramework": {
+ "@id": "https://schema.org/targetFramework",
+ "@type": "xsd:string"
+ },
+ "targetName": {
+ "@id": "https://schema.org/targetName",
+ "@type": "xsd:string"
+ },
+ "targetType": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#targetType",
+ "@type": "xsd:string"
+ },
+ "targetUrl": {
+ "@id": "https://schema.org/targetUrl",
+ "@type": "xsd:anyURI"
+ }
+ }
+ },
+ "Criteria": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Criteria"
+ },
+ "EndorsementCredential": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementCredential"
+ },
+ "EndorsementSubject": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementSubject",
+ "@context": {
+ "endorsementComment": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#endorsementComment",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Evidence": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Evidence",
+ "@context": {
+ "audience": {
+ "@id": "https://schema.org/audience",
+ "@type": "xsd:string"
+ },
+ "genre": {
+ "@id": "https://schema.org/genre",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "GeoCoordinates": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#GeoCoordinates",
+ "@context": {
+ "latitude": {
+ "@id": "https://schema.org/latitude",
+ "@type": "xsd:string"
+ },
+ "longitude": {
+ "@id": "https://schema.org/longitude",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "IdentifierEntry": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry",
+ "@context": {
+ "identifier": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifier",
+ "@type": "xsd:string"
+ },
+ "identifierType": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifierType",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "IdentityObject": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentityObject",
+ "@context": {
+ "hashed": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#hashed",
+ "@type": "xsd:boolean"
+ },
+ "identityHash": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identityHash",
+ "@type": "xsd:string"
+ },
+ "identityType": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identityType",
+ "@type": "xsd:string"
+ },
+ "salt": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#salt",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Image": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Image",
+ "@context": {
+ "caption": {
+ "@id": "https://schema.org/caption",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Profile": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile",
+ "@context": {
+ "additionalName": {
+ "@id": "https://schema.org/additionalName",
+ "@type": "xsd:string"
+ },
+ "address": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Address"
+ },
+ "dateOfBirth": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#dateOfBirth",
+ "@type": "xsd:date"
+ },
+ "email": {
+ "@id": "https://schema.org/email",
+ "@type": "xsd:string"
+ },
+ "familyName": {
+ "@id": "https://schema.org/familyName",
+ "@type": "xsd:string"
+ },
+ "familyNamePrefix": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#familyNamePrefix",
+ "@type": "xsd:string"
+ },
+ "givenName": {
+ "@id": "https://schema.org/givenName",
+ "@type": "xsd:string"
+ },
+ "honorificPrefix": {
+ "@id": "https://schema.org/honorificPrefix",
+ "@type": "xsd:string"
+ },
+ "honorificSuffix": {
+ "@id": "https://schema.org/honorificSuffix",
+ "@type": "xsd:string"
+ },
+ "otherIdentifier": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#otherIdentifier",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry",
+ "@container": "@set"
+ },
+ "parentOrg": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#parentOrg",
+ "@type": "xsd:string"
+ },
+ "patronymicName": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#patronymicName",
+ "@type": "xsd:string"
+ },
+ "phone": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#PhoneNumber",
+ "@type": "xsd:string"
+ },
+ "official": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#official",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Related": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Related",
+ "@context": {
+ "version": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#version",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "Result": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Result",
+ "@context": {
+ "achievedLevel": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#achievedLevel",
+ "@type": "xsd:anyURI"
+ },
+ "resultDescription": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultDescription",
+ "@type": "xsd:anyURI"
+ },
+ "status": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#status",
+ "@type": "xsd:string"
+ },
+ "value": {
+ "@id": "https://schema.org/value",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "ResultDescription": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#ResultDescription",
+ "@context": {
+ "allowedValue": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#allowedValue",
+ "@type": "xsd:string",
+ "@container": "@set"
+ },
+ "requiredLevel": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#requiredLevel",
+ "@type": "xsd:anyURI"
+ },
+ "requiredValue": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#requiredValue",
+ "@type": "xsd:string"
+ },
+ "resultType": {
+ "@id":"https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultType",
+ "@type": "xsd:string"
+ },
+ "rubricCriterionLevel": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#rubricCriterionLevel",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#RubricCriterionLevel",
+ "@container": "@set"
+ },
+ "valueMax": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#valueMax",
+ "@type": "xsd:string"
+ },
+ "valueMin": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#valueMin",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "RubricCriterionLevel": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#RubricCriterionLevel",
+ "@context": {
+ "level": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#level",
+ "@type": "xsd:string"
+ },
+ "points": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#points",
+ "@type": "xsd:string"
+ }
+ }
+ },
+ "alignment": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#alignment",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Alignment",
+ "@container": "@set"
+ },
+ "description": {
+ "@id": "https://schema.org/description",
+ "@type": "xsd:string"
+ },
+ "endorsement": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#endorsement",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementCredential",
+ "@container": "@set"
+ },
+ "image": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#image",
+ "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Image"
+ },
+ "name": {
+ "@id": "https://schema.org/name",
+ "@type": "xsd:string"
+ },
+ "narrative": {
+ "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#narrative",
+ "@type": "xsd:string"
+ },
+ "url": {
+ "@id": "https://schema.org/url",
+ "@type": "xsd:anyURI"
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/odrl.jsonld b/inspector-vc/src/main/resources/contexts/odrl.jsonld
new file mode 100644
index 0000000..1da5c13
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/odrl.jsonld
@@ -0,0 +1,301 @@
+{
+ "@context": {
+ "odrl": "http://www.w3.org/ns/odrl/2/",
+ "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+ "owl": "http://www.w3.org/2002/07/owl#",
+ "skos": "http://www.w3.org/2004/02/skos/core#",
+ "dct": "http://purl.org/dc/terms/",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "vcard": "http://www.w3.org/2006/vcard/ns#",
+ "foaf": "http://xmlns.com/foaf/0.1/",
+ "schema": "http://schema.org/",
+ "cc": "http://creativecommons.org/ns#",
+ "uid": "@id",
+ "type": "@type",
+ "Policy": "odrl:Policy",
+ "Rule": "odrl:Rule",
+ "profile": {
+ "@type": "@id",
+ "@id": "odrl:profile"
+ },
+ "inheritFrom": {
+ "@type": "@id",
+ "@id": "odrl:inheritFrom"
+ },
+ "ConflictTerm": "odrl:ConflictTerm",
+ "conflict": {
+ "@type": "@vocab",
+ "@id": "odrl:conflict"
+ },
+ "perm": "odrl:perm",
+ "prohibit": "odrl:prohibit",
+ "invalid": "odrl:invalid",
+ "Agreement": "odrl:Agreement",
+ "Assertion": "odrl:Assertion",
+ "Offer": "odrl:Offer",
+ "Privacy": "odrl:Privacy",
+ "Request": "odrl:Request",
+ "Set": "odrl:Set",
+ "Ticket": "odrl:Ticket",
+ "Asset": "odrl:Asset",
+ "AssetCollection": "odrl:AssetCollection",
+ "relation": {
+ "@type": "@id",
+ "@id": "odrl:relation"
+ },
+ "hasPolicy": {
+ "@type": "@id",
+ "@id": "odrl:hasPolicy"
+ },
+ "target": {
+ "@type": "@id",
+ "@id": "odrl:target"
+ },
+ "output": {
+ "@type": "@id",
+ "@id": "odrl:output"
+ },
+ "partOf": {
+ "@type": "@id",
+ "@id": "odrl:partOf"
+ },
+ "source": {
+ "@type": "@id",
+ "@id": "odrl:source"
+ },
+ "Party": "odrl:Party",
+ "PartyCollection": "odrl:PartyCollection",
+ "function": {
+ "@type": "@vocab",
+ "@id": "odrl:function"
+ },
+ "PartyScope": "odrl:PartyScope",
+ "assignee": {
+ "@type": "@id",
+ "@id": "odrl:assignee"
+ },
+ "assigner": {
+ "@type": "@id",
+ "@id": "odrl:assigner"
+ },
+ "assigneeOf": {
+ "@type": "@id",
+ "@id": "odrl:assigneeOf"
+ },
+ "assignerOf": {
+ "@type": "@id",
+ "@id": "odrl:assignerOf"
+ },
+ "attributedParty": {
+ "@type": "@id",
+ "@id": "odrl:attributedParty"
+ },
+ "attributingParty": {
+ "@type": "@id",
+ "@id": "odrl:attributingParty"
+ },
+ "compensatedParty": {
+ "@type": "@id",
+ "@id": "odrl:compensatedParty"
+ },
+ "compensatingParty": {
+ "@type": "@id",
+ "@id": "odrl:compensatingParty"
+ },
+ "consentingParty": {
+ "@type": "@id",
+ "@id": "odrl:consentingParty"
+ },
+ "consentedParty": {
+ "@type": "@id",
+ "@id": "odrl:consentedParty"
+ },
+ "informedParty": {
+ "@type": "@id",
+ "@id": "odrl:informedParty"
+ },
+ "informingParty": {
+ "@type": "@id",
+ "@id": "odrl:informingParty"
+ },
+ "trackingParty": {
+ "@type": "@id",
+ "@id": "odrl:trackingParty"
+ },
+ "trackedParty": {
+ "@type": "@id",
+ "@id": "odrl:trackedParty"
+ },
+ "contractingParty": {
+ "@type": "@id",
+ "@id": "odrl:contractingParty"
+ },
+ "contractedParty": {
+ "@type": "@id",
+ "@id": "odrl:contractedParty"
+ },
+ "Action": "odrl:Action",
+ "action": {
+ "@type": "@vocab",
+ "@id": "odrl:action"
+ },
+ "includedIn": {
+ "@type": "@id",
+ "@id": "odrl:includedIn"
+ },
+ "implies": {
+ "@type": "@id",
+ "@id": "odrl:implies"
+ },
+ "Permission": "odrl:Permission",
+ "permission": {
+ "@type": "@id",
+ "@id": "odrl:permission"
+ },
+ "Prohibition": "odrl:Prohibition",
+ "prohibition": {
+ "@type": "@id",
+ "@id": "odrl:prohibition"
+ },
+ "obligation": {
+ "@type": "@id",
+ "@id": "odrl:obligation"
+ },
+ "use": "odrl:use",
+ "grantUse": "odrl:grantUse",
+ "aggregate": "odrl:aggregate",
+ "annotate": "odrl:annotate",
+ "anonymize": "odrl:anonymize",
+ "archive": "odrl:archive",
+ "concurrentUse": "odrl:concurrentUse",
+ "derive": "odrl:derive",
+ "digitize": "odrl:digitize",
+ "display": "odrl:display",
+ "distribute": "odrl:distribute",
+ "execute": "odrl:execute",
+ "extract": "odrl:extract",
+ "give": "odrl:give",
+ "index": "odrl:index",
+ "install": "odrl:install",
+ "modify": "odrl:modify",
+ "move": "odrl:move",
+ "play": "odrl:play",
+ "present": "odrl:present",
+ "print": "odrl:print",
+ "read": "odrl:read",
+ "reproduce": "odrl:reproduce",
+ "sell": "odrl:sell",
+ "stream": "odrl:stream",
+ "textToSpeech": "odrl:textToSpeech",
+ "transfer": "odrl:transfer",
+ "transform": "odrl:transform",
+ "translate": "odrl:translate",
+ "Duty": "odrl:Duty",
+ "duty": {
+ "@type": "@id",
+ "@id": "odrl:duty"
+ },
+ "consequence": {
+ "@type": "@id",
+ "@id": "odrl:consequence"
+ },
+ "remedy": {
+ "@type": "@id",
+ "@id": "odrl:remedy"
+ },
+ "acceptTracking": "odrl:acceptTracking",
+ "attribute": "odrl:attribute",
+ "compensate": "odrl:compensate",
+ "delete": "odrl:delete",
+ "ensureExclusivity": "odrl:ensureExclusivity",
+ "include": "odrl:include",
+ "inform": "odrl:inform",
+ "nextPolicy": "odrl:nextPolicy",
+ "obtainConsent": "odrl:obtainConsent",
+ "reviewPolicy": "odrl:reviewPolicy",
+ "uninstall": "odrl:uninstall",
+ "watermark": "odrl:watermark",
+ "Constraint": "odrl:Constraint",
+ "LogicalConstraint": "odrl:LogicalConstraint",
+ "constraint": {
+ "@type": "@id",
+ "@id": "odrl:constraint"
+ },
+ "refinement": {
+ "@type": "@id",
+ "@id": "odrl:refinement"
+ },
+ "Operator": "odrl:Operator",
+ "operator": {
+ "@type": "@vocab",
+ "@id": "odrl:operator"
+ },
+ "RightOperand": "odrl:RightOperand",
+ "rightOperand": "odrl:rightOperand",
+ "rightOperandReference": {
+ "@type": "xsd:anyURI",
+ "@id": "odrl:rightOperandReference"
+ },
+ "LeftOperand": "odrl:LeftOperand",
+ "leftOperand": {
+ "@type": "@vocab",
+ "@id": "odrl:leftOperand"
+ },
+ "unit": "odrl:unit",
+ "dataType": {
+ "@type": "xsd:anyType",
+ "@id": "odrl:datatype"
+ },
+ "status": "odrl:status",
+ "absolutePosition": "odrl:absolutePosition",
+ "absoluteSpatialPosition": "odrl:absoluteSpatialPosition",
+ "absoluteTemporalPosition": "odrl:absoluteTemporalPosition",
+ "absoluteSize": "odrl:absoluteSize",
+ "count": "odrl:count",
+ "dateTime": "odrl:dateTime",
+ "delayPeriod": "odrl:delayPeriod",
+ "deliveryChannel": "odrl:deliveryChannel",
+ "elapsedTime": "odrl:elapsedTime",
+ "event": "odrl:event",
+ "fileFormat": "odrl:fileFormat",
+ "industry": "odrl:industry:",
+ "language": "odrl:language",
+ "media": "odrl:media",
+ "meteredTime": "odrl:meteredTime",
+ "payAmount": "odrl:payAmount",
+ "percentage": "odrl:percentage",
+ "product": "odrl:product",
+ "purpose": "odrl:purpose",
+ "recipient": "odrl:recipient",
+ "relativePosition": "odrl:relativePosition",
+ "relativeSpatialPosition": "odrl:relativeSpatialPosition",
+ "relativeTemporalPosition": "odrl:relativeTemporalPosition",
+ "relativeSize": "odrl:relativeSize",
+ "resolution": "odrl:resolution",
+ "spatial": "odrl:spatial",
+ "spatialCoordinates": "odrl:spatialCoordinates",
+ "systemDevice": "odrl:systemDevice",
+ "timeInterval": "odrl:timeInterval",
+ "unitOfCount": "odrl:unitOfCount",
+ "version": "odrl:version",
+ "virtualLocation": "odrl:virtualLocation",
+ "eq": "odrl:eq",
+ "gt": "odrl:gt",
+ "gteq": "odrl:gteq",
+ "lt": "odrl:lt",
+ "lteq": "odrl:lteq",
+ "neq": "odrl:neg",
+ "isA": "odrl:isA",
+ "hasPart": "odrl:hasPart",
+ "isPartOf": "odrl:isPartOf",
+ "isAllOf": "odrl:isAllOf",
+ "isAnyOf": "odrl:isAnyOf",
+ "isNoneOf": "odrl:isNoneOf",
+ "or": "odrl:or",
+ "xone": "odrl:xone",
+ "and": "odrl:and",
+ "andSequence": "odrl:andSequence",
+ "policyUsage": "odrl:policyUsage"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld
new file mode 100644
index 0000000..2bffafe
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld
@@ -0,0 +1,92 @@
+{
+ "@context": {
+ "@version": 1.1,
+ "id": "@id",
+ "type": "@type",
+ "BbsBlsSignature2020": {
+ "@id": "https://w3id.org/security#BbsBlsSignature2020",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "proofValue": "https://w3id.org/security#proofValue",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "BbsBlsSignatureProof2020": {
+ "@id": "https://w3id.org/security#BbsBlsSignatureProof2020",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "sec": "https://w3id.org/security#",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "Bls12381G2Key2020": "https://w3id.org/security#Bls12381G2Key2020"
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld
new file mode 100644
index 0000000..8500805
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld
@@ -0,0 +1,93 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "Ed25519VerificationKey2020": {
+ "@id": "https://w3id.org/security#Ed25519VerificationKey2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "publicKeyMultibase": {
+ "@id": "https://w3id.org/security#publicKeyMultibase",
+ "@type": "https://w3id.org/security#multibase"
+ }
+ }
+ },
+ "Ed25519Signature2020": {
+ "@id": "https://w3id.org/security#Ed25519Signature2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": {
+ "@id": "https://w3id.org/security#proofValue",
+ "@type": "https://w3id.org/security#multibase"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/security-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-v1.jsonld
new file mode 100644
index 0000000..7529505
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/security-v1.jsonld
@@ -0,0 +1,50 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+
+ "dc": "http://purl.org/dc/terms/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+ "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
+ "Ed25519Signature2018": "sec:Ed25519Signature2018",
+ "EncryptedMessage": "sec:EncryptedMessage",
+ "GraphSignature2012": "sec:GraphSignature2012",
+ "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
+ "LinkedDataSignature2016": "sec:LinkedDataSignature2016",
+ "CryptographicKey": "sec:Key",
+
+ "authenticationTag": "sec:authenticationTag",
+ "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
+ "cipherAlgorithm": "sec:cipherAlgorithm",
+ "cipherData": "sec:cipherData",
+ "cipherKey": "sec:cipherKey",
+ "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
+ "creator": {"@id": "dc:creator", "@type": "@id"},
+ "digestAlgorithm": "sec:digestAlgorithm",
+ "digestValue": "sec:digestValue",
+ "domain": "sec:domain",
+ "encryptionKey": "sec:encryptionKey",
+ "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+ "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
+ "initializationVector": "sec:initializationVector",
+ "iterationCount": "sec:iterationCount",
+ "nonce": "sec:nonce",
+ "normalizationAlgorithm": "sec:normalizationAlgorithm",
+ "owner": {"@id": "sec:owner", "@type": "@id"},
+ "password": "sec:password",
+ "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
+ "privateKeyPem": "sec:privateKeyPem",
+ "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
+ "publicKeyBase58": "sec:publicKeyBase58",
+ "publicKeyPem": "sec:publicKeyPem",
+ "publicKeyWif": "sec:publicKeyWif",
+ "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
+ "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
+ "salt": "sec:salt",
+ "signature": "sec:signature",
+ "signatureAlgorithm": "sec:signingAlgorithm",
+ "signatureValue": "sec:signatureValue"
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/security-v2.jsonld b/inspector-vc/src/main/resources/contexts/security-v2.jsonld
new file mode 100644
index 0000000..5f43a0c
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/security-v2.jsonld
@@ -0,0 +1,59 @@
+{
+ "@context": [{
+ "@version": 1.1
+ }, "https://w3id.org/security/v1", {
+ "AesKeyWrappingKey2019": "sec:AesKeyWrappingKey2019",
+ "DeleteKeyOperation": "sec:DeleteKeyOperation",
+ "DeriveSecretOperation": "sec:DeriveSecretOperation",
+ "EcdsaSecp256k1Signature2019": "sec:EcdsaSecp256k1Signature2019",
+ "EcdsaSecp256r1Signature2019": "sec:EcdsaSecp256r1Signature2019",
+ "EcdsaSecp256k1VerificationKey2019": "sec:EcdsaSecp256k1VerificationKey2019",
+ "EcdsaSecp256r1VerificationKey2019": "sec:EcdsaSecp256r1VerificationKey2019",
+ "Ed25519Signature2018": "sec:Ed25519Signature2018",
+ "Ed25519VerificationKey2018": "sec:Ed25519VerificationKey2018",
+ "EquihashProof2018": "sec:EquihashProof2018",
+ "ExportKeyOperation": "sec:ExportKeyOperation",
+ "GenerateKeyOperation": "sec:GenerateKeyOperation",
+ "KmsOperation": "sec:KmsOperation",
+ "RevokeKeyOperation": "sec:RevokeKeyOperation",
+ "RsaSignature2018": "sec:RsaSignature2018",
+ "RsaVerificationKey2018": "sec:RsaVerificationKey2018",
+ "Sha256HmacKey2019": "sec:Sha256HmacKey2019",
+ "SignOperation": "sec:SignOperation",
+ "UnwrapKeyOperation": "sec:UnwrapKeyOperation",
+ "VerifyOperation": "sec:VerifyOperation",
+ "WrapKeyOperation": "sec:WrapKeyOperation",
+ "X25519KeyAgreementKey2019": "sec:X25519KeyAgreementKey2019",
+
+ "allowedAction": "sec:allowedAction",
+ "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"},
+ "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"},
+ "capability": {"@id": "sec:capability", "@type": "@id"},
+ "capabilityAction": "sec:capabilityAction",
+ "capabilityChain": {"@id": "sec:capabilityChain", "@type": "@id", "@container": "@list"},
+ "capabilityDelegation": {"@id": "sec:capabilityDelegationMethod", "@type": "@id", "@container": "@set"},
+ "capabilityInvocation": {"@id": "sec:capabilityInvocationMethod", "@type": "@id", "@container": "@set"},
+ "caveat": {"@id": "sec:caveat", "@type": "@id", "@container": "@set"},
+ "challenge": "sec:challenge",
+ "ciphertext": "sec:ciphertext",
+ "controller": {"@id": "sec:controller", "@type": "@id"},
+ "delegator": {"@id": "sec:delegator", "@type": "@id"},
+ "equihashParameterK": {"@id": "sec:equihashParameterK", "@type": "xsd:integer"},
+ "equihashParameterN": {"@id": "sec:equihashParameterN", "@type": "xsd:integer"},
+ "invocationTarget": {"@id": "sec:invocationTarget", "@type": "@id"},
+ "invoker": {"@id": "sec:invoker", "@type": "@id"},
+ "jws": "sec:jws",
+ "keyAgreement": {"@id": "sec:keyAgreementMethod", "@type": "@id", "@container": "@set"},
+ "kmsModule": {"@id": "sec:kmsModule"},
+ "parentCapability": {"@id": "sec:parentCapability", "@type": "@id"},
+ "plaintext": "sec:plaintext",
+ "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"},
+ "proofPurpose": {"@id": "sec:proofPurpose", "@type": "@vocab"},
+ "proofValue": "sec:proofValue",
+ "referenceId": "sec:referenceId",
+ "unwrappedKey": "sec:unwrappedKey",
+ "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"},
+ "verifyData": "sec:verifyData",
+ "wrappedKey": "sec:wrappedKey"
+ }]
+}
diff --git a/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld b/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld
new file mode 100644
index 0000000..9c76d1a
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld
@@ -0,0 +1,710 @@
+{
+ "@context": [{
+ "@version": 1.1,
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "JsonWebKey2020": {
+ "@id": "https://w3id.org/security#JsonWebKey2020"
+ },
+ "JsonWebSignature2020": {
+ "@id": "https://w3id.org/security#JsonWebSignature2020",
+ "@context": {
+ "@version": 1.1,
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "Ed25519VerificationKey2020": {
+ "@id": "https://w3id.org/security#Ed25519VerificationKey2020"
+ },
+ "Ed25519Signature2020": {
+ "@id": "https://w3id.org/security#Ed25519Signature2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": {
+ "@id": "https://w3id.org/security#proofValue"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "publicKeyJwk": {
+ "@id": "https://w3id.org/security#publicKeyJwk",
+ "@type": "@json"
+ },
+ "ethereumAddress": {
+ "@id": "https://w3id.org/security#ethereumAddress"
+ },
+ "publicKeyHex": {
+ "@id": "https://w3id.org/security#publicKeyHex"
+ },
+ "blockchainAccountId": {
+ "@id": "https://w3id.org/security#blockchainAccountId"
+ },
+ "MerkleProof2019": {
+ "@id": "https://w3id.org/security#MerkleProof2019"
+ },
+ "Bls12381G1Key2020": {
+ "@id": "https://w3id.org/security#Bls12381G1Key2020"
+ },
+ "Bls12381G2Key2020": {
+ "@id": "https://w3id.org/security#Bls12381G2Key2020"
+ },
+ "BbsBlsSignature2020": {
+ "@id": "https://w3id.org/security#BbsBlsSignature2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "BbsBlsSignatureProof2020": {
+ "@id": "https://w3id.org/security#BbsBlsSignatureProof2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+
+ "EcdsaKoblitzSignature2016": "https://w3id.org/security#EcdsaKoblitzSignature2016",
+ "Ed25519Signature2018": {
+ "@id": "https://w3id.org/security#Ed25519Signature2018",
+ "@context": {
+ "@protected": true,
+
+ "id": "@id",
+ "type": "@type",
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "EncryptedMessage": "https://w3id.org/security#EncryptedMessage",
+ "GraphSignature2012": "https://w3id.org/security#GraphSignature2012",
+ "LinkedDataSignature2015": "https://w3id.org/security#LinkedDataSignature2015",
+ "LinkedDataSignature2016": "https://w3id.org/security#LinkedDataSignature2016",
+ "CryptographicKey": "https://w3id.org/security#Key",
+ "authenticationTag": "https://w3id.org/security#authenticationTag",
+ "canonicalizationAlgorithm": "https://w3id.org/security#canonicalizationAlgorithm",
+ "cipherAlgorithm": "https://w3id.org/security#cipherAlgorithm",
+ "cipherData": "https://w3id.org/security#cipherData",
+ "cipherKey": "https://w3id.org/security#cipherKey",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "creator": {
+ "@id": "http://purl.org/dc/terms/creator",
+ "@type": "@id"
+ },
+ "digestAlgorithm": "https://w3id.org/security#digestAlgorithm",
+ "digestValue": "https://w3id.org/security#digestValue",
+ "domain": "https://w3id.org/security#domain",
+ "encryptionKey": "https://w3id.org/security#encryptionKey",
+ "expiration": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "initializationVector": "https://w3id.org/security#initializationVector",
+ "iterationCount": "https://w3id.org/security#iterationCount",
+ "nonce": "https://w3id.org/security#nonce",
+ "normalizationAlgorithm": "https://w3id.org/security#normalizationAlgorithm",
+ "owner": "https://w3id.org/security#owner",
+ "password": "https://w3id.org/security#password",
+ "privateKey": "https://w3id.org/security#privateKey",
+ "privateKeyPem": "https://w3id.org/security#privateKeyPem",
+ "publicKey": "https://w3id.org/security#publicKey",
+ "publicKeyBase58": "https://w3id.org/security#publicKeyBase58",
+ "publicKeyPem": "https://w3id.org/security#publicKeyPem",
+ "publicKeyWif": "https://w3id.org/security#publicKeyWif",
+ "publicKeyService": "https://w3id.org/security#publicKeyService",
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "salt": "https://w3id.org/security#salt",
+ "signature": "https://w3id.org/security#signature",
+ "signatureAlgorithm": "https://w3id.org/security#signingAlgorithm",
+ "signatureValue": "https://w3id.org/security#signatureValue",
+ "proofValue": "https://w3id.org/security#proofValue",
+
+ "AesKeyWrappingKey2019": "https://w3id.org/security#AesKeyWrappingKey2019",
+ "DeleteKeyOperation": "https://w3id.org/security#DeleteKeyOperation",
+ "DeriveSecretOperation": "https://w3id.org/security#DeriveSecretOperation",
+ "EcdsaSecp256k1Signature2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019",
+ "@context": {
+ "@protected": true,
+
+ "id": "@id",
+ "type": "@type",
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "EcdsaSecp256r1Signature2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019",
+ "@context": {
+ "@protected": true,
+
+ "id": "@id",
+ "type": "@type",
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "EcdsaSecp256k1VerificationKey2019": "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019",
+ "EcdsaSecp256r1VerificationKey2019": "https://w3id.org/security#EcdsaSecp256r1VerificationKey2019",
+ "Ed25519VerificationKey2018": "https://w3id.org/security#Ed25519VerificationKey2018",
+ "EquihashProof2018": "https://w3id.org/security#EquihashProof2018",
+ "ExportKeyOperation": "https://w3id.org/security#ExportKeyOperation",
+ "GenerateKeyOperation": "https://w3id.org/security#GenerateKeyOperation",
+ "KmsOperation": "https://w3id.org/security#KmsOperation",
+ "RevokeKeyOperation": "https://w3id.org/security#RevokeKeyOperation",
+ "RsaSignature2018": {
+ "@id": "https://w3id.org/security#RsaSignature2018",
+ "@context": {
+ "@protected": true,
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": "https://w3id.org/security#proofValue",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ },
+ "RsaVerificationKey2018": "https://w3id.org/security#RsaVerificationKey2018",
+ "Sha256HmacKey2019": "https://w3id.org/security#Sha256HmacKey2019",
+ "SignOperation": "https://w3id.org/security#SignOperation",
+ "UnwrapKeyOperation": "https://w3id.org/security#UnwrapKeyOperation",
+ "VerifyOperation": "https://w3id.org/security#VerifyOperation",
+ "WrapKeyOperation": "https://w3id.org/security#WrapKeyOperation",
+ "X25519KeyAgreementKey2019": "https://w3id.org/security#X25519KeyAgreementKey2019",
+
+ "allowedAction": "https://w3id.org/security#allowedAction",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capability": {
+ "@id": "https://w3id.org/security#capability",
+ "@type": "@id"
+ },
+ "capabilityAction": "https://w3id.org/security#capabilityAction",
+ "capabilityChain": {
+ "@id": "https://w3id.org/security#capabilityChain",
+ "@type": "@id",
+ "@container": "@list"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "caveat": {
+ "@id": "https://w3id.org/security#caveat",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "challenge": "https://w3id.org/security#challenge",
+ "ciphertext": "https://w3id.org/security#ciphertext",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "delegator": {
+ "@id": "https://w3id.org/security#delegator",
+ "@type": "@id"
+ },
+ "equihashParameterK": {
+ "@id": "https://w3id.org/security#equihashParameterK",
+ "@type": "http://www.w3.org/2001/XMLSchema#:integer"
+ },
+ "equihashParameterN": {
+ "@id": "https://w3id.org/security#equihashParameterN",
+ "@type": "http://www.w3.org/2001/XMLSchema#:integer"
+ },
+ "invocationTarget": {
+ "@id": "https://w3id.org/security#invocationTarget",
+ "@type": "@id"
+ },
+ "invoker": {
+ "@id": "https://w3id.org/security#invoker",
+ "@type": "@id"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "kmsModule": {
+ "@id": "https://w3id.org/security#kmsModule"
+ },
+ "parentCapability": {
+ "@id": "https://w3id.org/security#parentCapability",
+ "@type": "@id"
+ },
+ "plaintext": "https://w3id.org/security#plaintext",
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "referenceId": "https://w3id.org/security#referenceId",
+ "unwrappedKey": "https://w3id.org/security#unwrappedKey",
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ },
+ "verifyData": "https://w3id.org/security#verifyData",
+ "wrappedKey": "https://w3id.org/security#wrappedKey"
+ }]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld b/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld
new file mode 100644
index 0000000..6533c28
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld
@@ -0,0 +1,91 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "Ed25519VerificationKey2018": {
+ "@id": "https://w3id.org/security#Ed25519VerificationKey2018",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "publicKeyBase58": {
+ "@id": "https://w3id.org/security#publicKeyBase58"
+ }
+ }
+ },
+ "Ed25519Signature2018": {
+ "@id": "https://w3id.org/security#Ed25519Signature2018",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "jws": {
+ "@id": "https://w3id.org/security#jws"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld b/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld
new file mode 100644
index 0000000..b74da8c
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld
@@ -0,0 +1,93 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "Ed25519VerificationKey2020": {
+ "@id": "https://w3id.org/security#Ed25519VerificationKey2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "publicKeyMultibase": {
+ "@id": "https://w3id.org/security#publicKeyMultibase",
+ "@type": "https://w3id.org/security#multibase"
+ }
+ }
+ },
+ "Ed25519Signature2020": {
+ "@id": "https://w3id.org/security#Ed25519Signature2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "proofValue": {
+ "@id": "https://w3id.org/security#proofValue",
+ "@type": "https://w3id.org/security#multibase"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld b/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld
new file mode 100644
index 0000000..17186cd
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld
@@ -0,0 +1,82 @@
+{
+ "@context": {
+ "privateKeyJwk": {
+ "@id": "https://w3id.org/security#privateKeyJwk",
+ "@type": "@json"
+ },
+ "JsonWebKey2020": {
+ "@id": "https://w3id.org/security#JsonWebKey2020",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "publicKeyJwk": {
+ "@id": "https://w3id.org/security#publicKeyJwk",
+ "@type": "@json"
+ }
+ }
+ },
+ "JsonWebSignature2020": {
+ "@id": "https://w3id.org/security#JsonWebSignature2020",
+ "@context": {
+ "@protected": true,
+
+ "id": "@id",
+ "type": "@type",
+
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "jws": "https://w3id.org/security#jws",
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+
+ "id": "@id",
+ "type": "@type",
+
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld b/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld
new file mode 100644
index 0000000..5a345df
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld
@@ -0,0 +1,102 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "proof": {
+ "@id": "https://w3id.org/security#proof",
+ "@type": "@id",
+ "@container": "@graph"
+ },
+ "EcdsaSecp256k1VerificationKey2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "blockchainAccountId": {
+ "@id": "https://w3id.org/security#blockchainAccountId"
+ },
+ "publicKeyJwk": {
+ "@id": "https://w3id.org/security#publicKeyJwk",
+ "@type": "@json"
+ },
+ "publicKeyBase58": {
+ "@id": "https://w3id.org/security#publicKeyBase58"
+ },
+ "publicKeyMultibase": {
+ "@id": "https://w3id.org/security#publicKeyMultibase",
+ "@type": "https://w3id.org/security#multibase"
+ }
+ }
+ },
+ "EcdsaSecp256k1Signature2019": {
+ "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "challenge": "https://w3id.org/security#challenge",
+ "created": {
+ "@id": "http://purl.org/dc/terms/created",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "domain": "https://w3id.org/security#domain",
+ "expires": {
+ "@id": "https://w3id.org/security#expiration",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "nonce": "https://w3id.org/security#nonce",
+ "proofPurpose": {
+ "@id": "https://w3id.org/security#proofPurpose",
+ "@type": "@vocab",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "assertionMethod": {
+ "@id": "https://w3id.org/security#assertionMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "authentication": {
+ "@id": "https://w3id.org/security#authenticationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityInvocation": {
+ "@id": "https://w3id.org/security#capabilityInvocationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "capabilityDelegation": {
+ "@id": "https://w3id.org/security#capabilityDelegationMethod",
+ "@type": "@id",
+ "@container": "@set"
+ },
+ "keyAgreement": {
+ "@id": "https://w3id.org/security#keyAgreementMethod",
+ "@type": "@id",
+ "@container": "@set"
+ }
+ }
+ },
+ "jws": {
+ "@id": "https://w3id.org/security#jws"
+ },
+ "verificationMethod": {
+ "@id": "https://w3id.org/security#verificationMethod",
+ "@type": "@id"
+ }
+ }
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld b/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld
new file mode 100644
index 0000000..d01bac0
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld
@@ -0,0 +1,26 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "@protected": true,
+ "X25519KeyAgreementKey2019": {
+ "@id": "https://w3id.org/security#X25519KeyAgreementKey2019",
+ "@context": {
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "controller": {
+ "@id": "https://w3id.org/security#controller",
+ "@type": "@id"
+ },
+ "revoked": {
+ "@id": "https://w3id.org/security#revoked",
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime"
+ },
+ "publicKeyBase58": {
+ "@id": "https://w3id.org/security#publicKeyBase58"
+ }
+ }
+ }
+ }
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java
new file mode 100644
index 0000000..02258fb
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java
@@ -0,0 +1,88 @@
+package org.oneedtech.inspect.vc;
+
+import java.io.StringWriter;
+import java.net.URI;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import com.apicatalog.did.key.DidKey;
+import com.apicatalog.ld.signature.ed25519.Ed25519Proof2020Adapter;
+import com.apicatalog.ld.signature.key.KeyPair;
+import com.apicatalog.ld.signature.proof.ProofOptions;
+import com.apicatalog.ld.signature.proof.VerificationMethod;
+import com.apicatalog.vc.Vc;
+import com.apicatalog.vc.processor.Issuer;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonWriter;
+import jakarta.json.JsonWriterFactory;
+import jakarta.json.stream.JsonGenerator;
+
+public class IronTests {
+
+ @Disabled
+ @Test
+ void testOb_01() {
+ Assertions.assertDoesNotThrow(()->{
+
+
+ final DidKey didKey = DidKey.from(URI.create("did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"));
+
+ //https://w3id.org/security#Ed25519KeyPair2020
+ //https://w3id.org/security#Ed25519Signature2020
+ URI unsigned = Samples.OB30.JSON.SIMPLE_JSON_NOPROOF.asURL().toURI();
+ KeyPair kp = Vc.generateKeys("https://w3id.org/security#Ed25519Signature2020").get(URI.create("urn:1"), 256);
+ ProofOptions options = ProofOptions.create(
+ Ed25519Proof2020Adapter.TYPE,
+ //new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")),
+ new VerificationMethod(didKey.toUri()),
+ URI.create("https://w3id.org/security#assertionMethod")).created(Instant.now().truncatedTo(ChronoUnit.SECONDS));
+
+ Issuer issuer = Vc.sign(unsigned, kp, options);
+ System.err.println(pretty(issuer.getCompacted()));
+ JsonObject signed = issuer.getCompacted();
+ JsonObject proof = signed.getJsonObject("sec:proof");
+ Assertions.assertNotNull(proof);
+
+ Vc.verify(issuer.getCompacted()).isValid();
+ });
+ }
+
+ @Disabled
+ @Test
+ void testClr_01() {
+ Assertions.assertDoesNotThrow(()->{
+ URI unsigned = Samples.CLR20.JSON.SIMPLE_JSON_NOPROOF.asURL().toURI();
+ KeyPair kp = Vc.generateKeys("https://w3id.org/security#Ed25519Signature2020").get(URI.create("urn:1"), 256);
+ ProofOptions options = ProofOptions.create(
+ Ed25519Proof2020Adapter.TYPE,
+ new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")),
+ URI.create("https://w3id.org/security#assertionMethod"));
+
+ Issuer issuer = Vc.sign(unsigned, kp, options);
+ JsonObject job = issuer.getCompacted().getJsonObject("sec:proof");
+
+ //System.err.println (pretty(issuer.getCompacted().toString()));
+ Assertions.assertNotNull(job);
+ Vc.verify(issuer.getCompacted()).isValid();
+ });
+ }
+
+ String pretty(JsonObject jobj) {
+ Map properties = new HashMap<>(1);
+ properties.put(JsonGenerator.PRETTY_PRINTING, true);
+ StringWriter sw = new StringWriter();
+ JsonWriterFactory writerFactory = Json.createWriterFactory(properties);
+ JsonWriter jsonWriter = writerFactory.createWriter(sw);
+ jsonWriter.writeObject(jobj);
+ jsonWriter.close();
+ return sw.toString();
+ }
+}
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
new file mode 100644
index 0000000..0be0215
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java
@@ -0,0 +1,174 @@
+package org.oneedtech.inspect.vc;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.oneedtech.inspect.test.Assertions.*;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+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.ExpirationProbe;
+import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceProbe;
+import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe;
+import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
+
+import com.google.common.collect.Iterables;
+
+public class OB30Tests {
+ private static OB30Inspector validator;
+ private static boolean verbose = true;
+
+ @BeforeAll
+ static void setup() {
+ validator = new OB30Inspector.Builder()
+ .set(Behavior.TEST_INCLUDE_SUCCESS, true)
+ .set(Behavior.VALIDATOR_FAIL_FAST, true)
+ .build();
+ }
+
+ @Test
+ void testSimpleJsonValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimplePNGPlainValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimplePNGJWTValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimpleJsonSVGPlainValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimpleJsonSVGJWTValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimpleJsonInvalidUnknownType() {
+ //add a dumb value to .type and remove the ob type
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_UNKNOWN_TYPE.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertFatalCount(report, 1);
+ assertHasProbeID(report, TypePropertyProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testSimpleJsonInvalidProofMethod() {
+ //add some garbage chars to proofValue
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_METHOD_ERROR.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertErrorCount(report, 1);
+ assertHasProbeID(report, EmbeddedProofProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testSimpleJsonInvalidProofValue() {
+ //add some garbage chars to proofValue
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_VALUE_ERROR.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertErrorCount(report, 1);
+ assertHasProbeID(report, EmbeddedProofProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testSimpleJsonExpired() {
+ //"expirationDate": "2020-01-20T00:00:00Z",
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_EXPIRED.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertHasProbeID(report, ExpirationProbe.ID, true);
+ });
+ }
+
+ @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
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ISSUER.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertHasProbeID(report, JsonSchemaProbe.ID, true);
+ });
+ }
+
+ @Disabled //TODO IssuanceVerifierProbe is not run because FATAL: InvalidSignature terminates
+ @Test
+ void testSimpleJsonNotIssued() {
+ //"issuanceDate": "2040-01-01T00:00:00Z",
+ //this breaks the proof too
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ISSUED.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertHasProbeID(report, IssuanceProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testCompleteJsonInvalidInlineSchemaRef() throws Exception {
+ //404 inline schema ref, and 404 refresh uri
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.COMPLETE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertFalse(report.asBoolean());
+ assertTrue(Iterables.size(report.getErrors()) > 0);
+ assertTrue(Iterables.size(report.getExceptions()) > 0);
+ assertHasProbeID(report, InlineJsonSchemaProbe.ID, true);
+ });
+ }
+
+}
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
new file mode 100644
index 0000000..3c10f32
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
@@ -0,0 +1,39 @@
+package org.oneedtech.inspect.vc;
+
+import org.oneedtech.inspect.test.Sample;
+
+public class Samples {
+ public static final class OB30 {
+ public static final class SVG {
+ public final static Sample SIMPLE_JSON_SVG = new Sample("ob30/simple-json.svg", true);
+ public final static Sample SIMPLE_JWT_SVG = new Sample("ob30/simple-jwt.svg", true);
+ }
+ 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_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_PROOF_METHOD_ERROR = new Sample("ob30/simple-err-proof-method.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_VALUE_ERROR = new Sample("ob30/simple-err-proof-value.json", false);
+ 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);
+ public final static Sample SIMPLE_JSON_PNG = new Sample("ob30/simple-json.png", true);
+ }
+ public static final class JWT {
+ public final static Sample SIMPLE_JWT = new Sample("ob30/simple.jwt", true);
+ }
+ }
+
+ public static final class CLR20 {
+ public static final class JSON {
+ public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true);
+ public final static Sample SIMPLE_JSON_NOPROOF = new Sample("clr20/simple-noproof.json", true);
+ public final static Sample SIMPLE_JWT = new Sample("clr20/simple.jwt", true);
+ }
+ }
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java
new file mode 100644
index 0000000..28b8e7c
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java
@@ -0,0 +1,119 @@
+package org.oneedtech.inspect.vc.credential;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.RunContext.Key;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.vc.Credential;
+import org.oneedtech.inspect.vc.OB30Inspector;
+import org.oneedtech.inspect.vc.Samples;
+import org.oneedtech.inspect.vc.payload.PayloadParser;
+import org.oneedtech.inspect.vc.payload.PayloadParserFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class PayloadParserTests {
+
+ @Test
+ void testSvgStringExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource(ResourceType.SVG);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ @Test
+ void testSvgJwtExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ @Test
+ void testPngStringExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource(ResourceType.PNG);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ @Test
+ void testPngJwtExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource(ResourceType.PNG);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ @Test
+ void testJwtExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.JWT.SIMPLE_JWT.asFileResource(ResourceType.JWT);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ @Test
+ void testJsonExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.JSON.SIMPLE_JSON.asFileResource(ResourceType.JSON);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ Credential crd = ext.parse(res, mockOB30Context(res));
+ //System.out.println(crd.getJson().toPrettyString());
+ assertNotNull(crd);
+ assertNotNull(crd.getJson());
+ assertNotNull(crd.getJson().get("@context"));
+ });
+ }
+
+ private RunContext mockOB30Context(Resource res) {
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+ return new RunContext.Builder()
+ .put(new OB30Inspector.Builder().build())
+ .put(res)
+ .put(Key.JACKSON_OBJECTMAPPER, mapper)
+ .put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .build();
+ }
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java
new file mode 100644
index 0000000..b56542f
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java
@@ -0,0 +1,34 @@
+package org.oneedtech.inspect.vc.util;
+
+import java.net.URI;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.apicatalog.jsonld.document.Document;
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
+
+public class CachingDocumentLoaderTests {
+
+ @Test
+ void testStaticCachedDocumentBundled() {
+ Assertions.assertDoesNotThrow(()->{
+ DocumentLoader loader = new CachingDocumentLoader();
+ for(String id : CachingDocumentLoader.bundled.keySet()) {
+ Document doc = loader.loadDocument(new URI(id), new DocumentLoaderOptions());
+ Assertions.assertNotNull(doc);
+ }
+ });
+ }
+
+ @Test
+ void testStaticCachedDocumentKey() {
+ Assertions.assertDoesNotThrow(()->{
+ DocumentLoader loader = new CachingDocumentLoader();
+ URI uri = new URI("https://www.w3.org/ns/did/v1");
+ Document doc = loader.loadDocument(uri, new DocumentLoaderOptions());
+ Assertions.assertNotNull(doc);
+ });
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java
new file mode 100644
index 0000000..762bd65
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java
@@ -0,0 +1,43 @@
+package org.oneedtech.inspect.vc.util;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.vc.Samples;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
+public class JsonNodeUtilTests {
+ static final ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ static final JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+ @Test
+ void testFlattenNodeList() {
+ Assertions.assertDoesNotThrow(()->{
+ String json = Samples.OB30.JSON.COMPLETE_JSON.asString();
+ JsonNode root = mapper.readTree(json);
+ List list = JsonNodeUtil.asNodeList(root, "$..endorsement", jsonPath);
+ Assertions.assertEquals(5, list.size());
+ for(JsonNode node : list) {
+ ArrayNode types = (ArrayNode) node.get("type");
+ boolean found = false;
+ for(JsonNode val : types) {
+ if(val.asText().equals("EndorsementCredential")) {
+ found = true;
+ }
+ }
+ assertTrue(found);
+ }
+
+ });
+ }
+
+}
diff --git a/inspector-vc/src/test/resources/ob30/complete.json b/inspector-vc/src/test/resources/ob30/complete.json
new file mode 100644
index 0000000..92a16e8
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/complete.json
@@ -0,0 +1,712 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": [
+ "VerifiableCredential",
+ "OpenBadgeCredential"
+ ],
+ "name": "1EdTech University Degree for Example Student",
+ "description": "1EdTech University Degree Description",
+ "image": {
+ "id": "https://1edtech.edu/credentials/3732/image",
+ "type": "Image",
+ "caption": "1EdTech University Degree for Example Student"
+ },
+ "credentialSubject": {
+ "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
+ "type": [
+ "AchievementSubject"
+ ],
+ "activityEndDate": "2010-01-02T00:00:00Z",
+ "activityStartDate": "2010-01-01T00:00:00Z",
+ "creditsEarned": 42,
+ "licenseNumber": "A-9320041",
+ "role": "Major Domo",
+ "source": {
+ "id": "https://school.edu/issuers/201234",
+ "type": [
+ "Profile"
+ ],
+ "name": "1EdTech College of Arts"
+ },
+ "term": "Fall",
+ "identifier": [
+ {
+ "type": "IdentityObject",
+ "identityHash": "student@1edtech.edu",
+ "identityType": "email",
+ "hashed": false,
+ "salt": "not-used"
+ },
+ {
+ "type": "IdentityObject",
+ "identityHash": "somebody@gmail.com",
+ "identityType": "email",
+ "hashed": false,
+ "salt": "not-used"
+ }
+ ],
+ "achievement": {
+ "id": "https://1edtech.edu/achievements/degree",
+ "type": [
+ "Achievement"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "degree",
+ "targetDescription": "1EdTech University Degree programs.",
+ "targetName": "1EdTech University Degree",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFItem",
+ "targetUrl": "https://1edtech.edu/catalog/degree"
+ },
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "degree",
+ "targetDescription": "1EdTech University Degree programs.",
+ "targetName": "1EdTech University Degree",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CTDL",
+ "targetUrl": "https://credentialengineregistry.org/resources/ce-98cb027b-95ef-4494-908d-6f7790ec6b6b"
+ }
+ ],
+ "achievementType": "Degree",
+ "creator": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "1EdTech University",
+ "url": "https://1edtech.edu",
+ "phone": "1-222-333-4444",
+ "description": "1EdTech University provides online degree programs.",
+ "endorsement": [
+ {
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "EndorsementCredential"
+ ],
+ "issuer": {
+ "id": "https://accrediter.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "Example Accrediting Agency"
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSubject": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "EndorsementSubject"
+ ],
+ "endorsementComment": "1EdTech University is in good standing"
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/endorsementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ },
+ {
+ "id": "https://accrediter.edu/schema/endorsementcredential.json",
+ "type": "JsonSchemaValidator2018"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://1edtech.edu/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-05-26T18:17:08Z",
+ "verificationMethod": "https://accrediter.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA"
+ }
+ ]
+ },
+ {
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "EndorsementCredential"
+ ],
+ "issuer": {
+ "id": "https://state.gov/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "State Department of Education"
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSubject": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "EndorsementSubject"
+ ],
+ "endorsementComment": "1EdTech University is in good standing"
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/endorsementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ },
+ {
+ "id": "https://state.gov/schema/endorsementcredential.json",
+ "type": "JsonSchemaValidator2018"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://state.gov/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://state.gov/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-05-26T18:25:59Z",
+ "verificationMethod": "https://accrediter.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z5bDnmSgDczXwZGya6ZjxKaxkdKxzsCMiVSsgEVWxnaWK7ZqbKnzcCd7mUKE9DQaAL2QMXP5AquPeW6W2CWrZ7jNC"
+ }
+ ]
+ }
+ ],
+ "image": {
+ "id": "https://1edtech.edu/logo.png",
+ "type": "Image",
+ "caption": "1EdTech University logo"
+ },
+ "email": "registrar@1edtech.edu",
+ "address": {
+ "type": [
+ "Address"
+ ],
+ "addressCountry": "USA",
+ "addressCountryCode": "US",
+ "addressRegion": "TX",
+ "addressLocality": "Austin",
+ "streetAddress": "123 First St",
+ "postOfficeBoxNumber": "1",
+ "postalCode": "12345",
+ "geo": {
+ "type": "GeoCoordinates",
+ "latitude": 1,
+ "longitude": 1
+ }
+ },
+ "otherIdentifier": [
+ {
+ "type": "IdentifierEntry",
+ "identifier": "12345",
+ "identifierType": "sourcedId"
+ },
+ {
+ "type": "IdentifierEntry",
+ "identifier": "67890",
+ "identifierType": "nationalIdentityNumber"
+ }
+ ],
+ "official": "Horace Mann",
+ "parentOrg": {
+ "id": "did:example:123456789",
+ "type": [
+ "Profile"
+ ],
+ "name": "Universal Universities"
+ }
+ },
+ "creditsAvailable": 36,
+ "criteria": {
+ "id": "https://1edtech.edu/achievements/degree",
+ "narrative": "# Degree Requirements\nStudents must complete..."
+ },
+ "description": "1EdTech University Degree Description",
+ "endorsement": [
+ {
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "EndorsementCredential"
+ ],
+ "issuer": {
+ "id": "https://accrediter.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "Example Accrediting Agency"
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSubject": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "EndorsementSubject"
+ ],
+ "endorsementComment": "1EdTech University is in good standing"
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/endorsementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ },
+ {
+ "id": "https://accrediter.edu/schema/endorsementcredential.json",
+ "type": "JsonSchemaValidator2018"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://1edtech.edu/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-05-26T18:17:08Z",
+ "verificationMethod": "https://accrediter.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA"
+ }
+ ]
+ }
+ ],
+ "fieldOfStudy": "Research",
+ "humanCode": "R1",
+ "image": {
+ "id": "https://1edtech.edu/achievements/degree/image",
+ "type": "Image",
+ "caption": "1EdTech University Degree"
+ },
+ "name": "1EdTech University Degree",
+ "otherIdentifier": [
+ {
+ "type": "IdentifierEntry",
+ "identifier": "abde",
+ "identifierType": "identifier"
+ }
+ ],
+ "resultDescription": [
+ {
+ "id": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c",
+ "type": [
+ "ResultDescription"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFItem",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project"
+ }
+ ],
+ "allowedValue": [
+ "D",
+ "C",
+ "B",
+ "A"
+ ],
+ "name": "Final Project Grade",
+ "requiredValue": "C",
+ "resultType": "LetterGrade"
+ },
+ {
+ "id": "urn:uuid:a70ddc6a-4c4a-4bd8-8277-cb97c79f40c5",
+ "type": [
+ "ResultDescription"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFItem",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project"
+ }
+ ],
+ "allowedValue": [
+ "D",
+ "C",
+ "B",
+ "A"
+ ],
+ "name": "Final Project Grade",
+ "requiredLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a",
+ "resultType": "RubricCriterionLevel",
+ "rubricCriterionLevel": [
+ {
+ "id": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a",
+ "type": [
+ "RubricCriterionLevel"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFRubricCriterionLevel",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/mastered"
+ }
+ ],
+ "description": "The author demonstrated...",
+ "level": "Mastered",
+ "name": "Mastery",
+ "points": "4"
+ },
+ {
+ "id": "urn:uuid:6b84b429-31ee-4dac-9d20-e5c55881f80e",
+ "type": [
+ "RubricCriterionLevel"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFRubricCriterionLevel",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/basic"
+ }
+ ],
+ "description": "The author demonstrated...",
+ "level": "Basic",
+ "name": "Basic",
+ "points": "4"
+ }
+ ]
+ },
+ {
+ "id": "urn:uuid:b07c0387-f2d6-4b65-a3f4-f4e4302ea8f7",
+ "type": [
+ "ResultDescription"
+ ],
+ "name": "Project Status",
+ "resultType": "Status"
+ }
+ ],
+ "specialization": "Computer Science Research",
+ "tag": [
+ "research",
+ "computer science"
+ ]
+ },
+ "image": {
+ "id": "https://1edtech.edu/credentials/3732/image",
+ "type": "Image",
+ "caption": "1EdTech University Degree for Example Student"
+ },
+ "narrative": "There is a final project report and source code evidence.",
+ "result": [
+ {
+ "type": [
+ "Result"
+ ],
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFItem",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1"
+ }
+ ],
+ "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c",
+ "value": "A"
+ },
+ {
+ "type": [
+ "Result"
+ ],
+ "achievedLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a",
+ "alignment": [
+ {
+ "type": [
+ "Alignment"
+ ],
+ "targetCode": "project",
+ "targetDescription": "Project description",
+ "targetName": "Final Project",
+ "targetFramework": "1EdTech University Program and Course Catalog",
+ "targetType": "CFItem",
+ "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1"
+ }
+ ],
+ "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c"
+ },
+ {
+ "type": [
+ "Result"
+ ],
+ "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c",
+ "status": "Completed"
+ }
+ ]
+ },
+ "endorsement": [
+ {
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "EndorsementCredential"
+ ],
+ "issuer": {
+ "id": "https://accrediter.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "Example Accrediting Agency"
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSubject": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "EndorsementSubject"
+ ],
+ "endorsementComment": "1EdTech University is in good standing"
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/endorsementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ },
+ {
+ "id": "https://accrediter.edu/schema/endorsementcredential.json",
+ "type": "JsonSchemaValidator2018"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://1edtech.edu/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-05-26T18:17:08Z",
+ "verificationMethod": "https://accrediter.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA"
+ }
+ ]
+ }
+ ],
+ "evidence": [
+ {
+ "id": "https://1edtech.edu/credentials/3732/evidence/1",
+ "type": [
+ "Evidence"
+ ],
+ "narrative": "# Final Project Report \n This project was ...",
+ "name": "Final Project Report",
+ "description": "This is the final project report.",
+ "genre": "Research",
+ "audience": "Department"
+ },
+ {
+ "id": "https://github.com/somebody/project",
+ "type": [
+ "Evidence"
+ ],
+ "name": "Final Project Code",
+ "description": "This is the source code for the final project app.",
+ "genre": "Research",
+ "audience": "Department"
+ }
+ ],
+ "issuer": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "1EdTech University",
+ "url": "https://1edtech.edu",
+ "phone": "1-222-333-4444",
+ "description": "1EdTech University provides online degree programs.",
+ "endorsement": [
+ {
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "type": [
+ "VerifiableCredential",
+ "EndorsementCredential"
+ ],
+ "issuer": {
+ "id": "https://accrediter.edu/issuers/565049",
+ "type": [
+ "Profile"
+ ],
+ "name": "Example Accrediting Agency"
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSubject": {
+ "id": "https://1edtech.edu/issuers/565049",
+ "type": [
+ "EndorsementSubject"
+ ],
+ "endorsementComment": "1EdTech University is in good standing"
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/endorsementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ },
+ {
+ "id": "https://accrediter.edu/schema/endorsementcredential.json",
+ "type": "JsonSchemaValidator2018"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://1edtech.edu/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-05-26T18:17:08Z",
+ "verificationMethod": "https://accrediter.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA"
+ }
+ ]
+ }
+ ],
+ "image": {
+ "id": "https://1edtech.edu/logo.png",
+ "type": "Image",
+ "caption": "1EdTech University logo"
+ },
+ "email": "registrar@1edtech.edu",
+ "address": {
+ "type": [
+ "Address"
+ ],
+ "addressCountry": "USA",
+ "addressCountryCode": "US",
+ "addressRegion": "TX",
+ "addressLocality": "Austin",
+ "streetAddress": "123 First St",
+ "postOfficeBoxNumber": "1",
+ "postalCode": "12345",
+ "geo": {
+ "type": "GeoCoordinates",
+ "latitude": 1,
+ "longitude": 1
+ }
+ },
+ "otherIdentifier": [
+ {
+ "type": "IdentifierEntry",
+ "identifier": "12345",
+ "identifierType": "sourcedId"
+ },
+ {
+ "type": "IdentifierEntry",
+ "identifier": "67890",
+ "identifierType": "nationalIdentityNumber"
+ }
+ ],
+ "official": "Horace Mann",
+ "parentOrg": {
+ "id": "did:example:123456789",
+ "type": [
+ "Profile"
+ ],
+ "name": "Universal Universities"
+ }
+ },
+ "issuanceDate": "2010-01-01T00:00:00Z",
+ "expirationDate": "2020-01-01T00:00:00Z",
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/achievementcredential.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ }
+ ],
+ "credentialStatus": {
+ "id": "https://1edtech.edu/credentials/3732/revocations",
+ "type": "1EdTechRevocationList"
+ },
+ "refreshService": {
+ "id": "http://1edtech.edu/credentials/3732",
+ "type": "1EdTechCredentialRefresh"
+ },
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-06-09T22:56:28Z",
+ "verificationMethod": "https://1edtech.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "zPpg92pBBEqRMxAqHMFQJ6Kmwf1thF9GdzqCofyWTLE6AhuahQixBNuG9BLgk6vb8K3NoqzanajYYYJbEcEhvQtM"
+ }
+ ]
+}
\ No newline at end of file
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
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-expired.json b/inspector-vc/src/test/resources/ob30/simple-err-expired.json
new file mode 100644
index 0000000..80c6959
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-expired.json
@@ -0,0 +1,37 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "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",
+ "expirationDate": "2020-01-20T00: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
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-issued.json b/inspector-vc/src/test/resources/ob30/simple-err-issued.json
new file mode 100644
index 0000000..e129e3d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-issued.json
@@ -0,0 +1,37 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "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": "2040-01-01T00:00:00Z",
+ "expirationDate": "2050-01-20T00: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
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-issuer.json b/inspector-vc/src/test/resources/ob30/simple-err-issuer.json
new file mode 100644
index 0000000..2e988ea
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-issuer.json
@@ -0,0 +1,29 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "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"
+ ],
+ "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
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json b/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json
new file mode 100644
index 0000000..037a10c
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json
@@ -0,0 +1,54 @@
+{
+ "@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://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"
+ ],
+ "achievement": {
+ "id": "https://example.com/achievements/21st-century-skills/teamwork",
+ "type": [
+ "Achievement"
+ ],
+ "criteria": {
+ "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
+ },
+ "description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
+ "name": "Teamwork"
+ }
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ }
+ ],
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-09-15T15:48:32Z",
+ "verificationMethod": "https://example.edu/issuers/565049#xxMkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z3yUuWbFsLUp2CUrSZRaRbTk1UnkhpoJgJYu1SdMqd3AEMotpY41sKky7VzavnSfjApggtWJg1tcREvs5H4ZNnBRH"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json b/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json
new file mode 100644
index 0000000..d26f49d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json
@@ -0,0 +1,54 @@
+{
+ "@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://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"
+ ],
+ "achievement": {
+ "id": "https://example.com/achievements/21st-century-skills/teamwork",
+ "type": [
+ "Achievement"
+ ],
+ "criteria": {
+ "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
+ },
+ "description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
+ "name": "Teamwork"
+ }
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ }
+ ],
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-09-15T15:48:32Z",
+ "verificationMethod": "https://example.edu/issuers/565049#z6MkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z3fQCWGpz7b1HSH6DTwYiH5vutqtpJb5SHiP1VFK22xeBEW2D61tC9j3SktwPLNxPnTNZnPt4GeAZJPdVYserRqs4"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple-err-type.json b/inspector-vc/src/test/resources/ob30/simple-err-type.json
new file mode 100644
index 0000000..a54f5c1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-err-type.json
@@ -0,0 +1,36 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "https://imsglobal.github.io/openbadges-specification/context.json",
+ "https://w3id.org/security/suites/ed25519-2020/v1"
+ ],
+ "id": "http://example.edu/credentials/3732",
+ "type": [
+ "VerifiableCredential",
+ "OtherCredential"
+ ],
+ "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-09T22:56:28Z",
+ "verificationMethod": "https://example.edu/issuers/565049#key-1",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple-json.png b/inspector-vc/src/test/resources/ob30/simple-json.png
new file mode 100644
index 0000000..ee80f0a
Binary files /dev/null and b/inspector-vc/src/test/resources/ob30/simple-json.png differ
diff --git a/inspector-vc/src/test/resources/ob30/simple-json.svg b/inspector-vc/src/test/resources/ob30/simple-json.svg
new file mode 100644
index 0000000..4a4f39c
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-json.svg
@@ -0,0 +1,74 @@
+
+
+
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple-jwt.png b/inspector-vc/src/test/resources/ob30/simple-jwt.png
new file mode 100644
index 0000000..7211f53
Binary files /dev/null and b/inspector-vc/src/test/resources/ob30/simple-jwt.png differ
diff --git a/inspector-vc/src/test/resources/ob30/simple-jwt.svg b/inspector-vc/src/test/resources/ob30/simple-jwt.svg
new file mode 100644
index 0000000..1f1d17f
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-jwt.svg
@@ -0,0 +1,17 @@
+
+
+
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple-noproof.json b/inspector-vc/src/test/resources/ob30/simple-noproof.json
new file mode 100644
index 0000000..0496844
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-noproof.json
@@ -0,0 +1,27 @@
+{
+ "@context": [
+ "https://www.w3.org/2018/credentials/v1",
+ "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"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple.json b/inspector-vc/src/test/resources/ob30/simple.json
new file mode 100644
index 0000000..6c48673
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple.json
@@ -0,0 +1,54 @@
+{
+ "@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://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"
+ ],
+ "achievement": {
+ "id": "https://example.com/achievements/21st-century-skills/teamwork",
+ "type": [
+ "Achievement"
+ ],
+ "criteria": {
+ "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management."
+ },
+ "description": "This badge recognizes the development of the capacity to collaborate within a group environment.",
+ "name": "Teamwork"
+ }
+ },
+ "credentialSchema": [
+ {
+ "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json",
+ "type": "1EdTechJsonSchemaValidator2019"
+ }
+ ],
+ "proof": [
+ {
+ "type": "Ed25519Signature2020",
+ "created": "2022-09-15T15:48:32Z",
+ "verificationMethod": "https://example.edu/issuers/565049#z6MkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj",
+ "proofPurpose": "assertionMethod",
+ "proofValue": "z3yUuWbFsLUp2CUrSZRaRbTk1UnkhpoJgJYu1SdMqd3AEMotpY41sKky7VzavnSfjApggtWJg1tcREvs5H4ZNnBRH"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob30/simple.jwt b/inspector-vc/src/test/resources/ob30/simple.jwt
new file mode 100644
index 0000000..6f64aad
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple.jwt
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaW1zZ2xvYmFsLmdpdGh1Yi5pby9vcGVuYmFkZ2VzLXNwZWNpZmljYXRpb24vY29udGV4dC5qc29uIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJFeGFtcGxlIFVuaXZlcnNpdHkifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IERlZ3JlZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.G7W8od9rSZRsVyk26rXjg_fH2CyUihwNpepd6tWgLt_UHC1vUU0Clox8IicnOSkMyYEqAuNZAdCC9_35i1oUcyj1c076Aa0dsVQ2fFVuQPqXBlyZWcBmo5jqOK6R9NHzRAYXwLRXgrB8gz3lSK55cnHTnMtkpXXcUcHkS5ylWbXCLeOWKoygOCuxRN3N6kP-0HOyuk15PWlnkJ2zEKz2pBtVPaNEydcT0kEtoHFMEWVwqo6rnGV-Ea3M7ssDt3145mcl-DVYLXmBVdT8KoO47QAOBaVMR6k-hgrHNBcdhpI-o6IvLIFsGLgrNvWN67i8Z7Baum1mP-HBpsAigdmIpA
\ No newline at end of file