diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml
new file mode 100644
index 0000000..6881751
--- /dev/null
+++ b/inspector-vc/pom.xml
@@ -0,0 +1,15 @@
+
+ 4.0.0
+
+ org.1edtech
+ inspector
+ 0.9.2
+
+ inspector-vc
+
+
+ org.1edtech
+ inspector-core
+
+
+
\ 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..63205c8
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java
@@ -0,0 +1,107 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.util.code.Defensives.*;
+import static org.oneedtech.inspect.util.resource.ResourceType.*;
+
+import java.util.Iterator;
+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;
+
+/**
+ * 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;
+
+ public Credential(Resource resource, JsonNode data) {
+ super(ID, GeneratedObject.Type.INTERNAL);
+ checkNotNull(resource, resource.getType(), data);
+ ResourceType type = resource.getType();
+ checkTrue(type == SVG || type == PNG || type == JSON || type == JWT,
+ "Unrecognized payload type: " + type.getName());
+ this.resource = resource;
+ this.jsonData = data;
+
+ ArrayNode typeNode = (ArrayNode)jsonData.get("type");
+ this.credentialType = Credential.Type.valueOf(typeNode);
+
+ }
+
+ public Resource getResource() {
+ return resource;
+ }
+
+ public JsonNode asJson() {
+ return jsonData;
+ }
+
+ public Credential.Type getCredentialType() {
+ return credentialType;
+ }
+
+ /**
+ * Get the canonical schema for this credential if such exists.
+ */
+ public Optional getSchemaKey() {
+ if(credentialType == Credential.Type.AchievementCredential) {
+ return Optional.of(Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON);
+ } else if(credentialType == Credential.Type.VerifiablePresentation) {
+ return Optional.of(Catalog.OB_30_VERIFIABLEPRESENTATION_JSON);
+ } else if(credentialType == Credential.Type.EndorsementCredential) {
+ return Optional.of(Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON);
+ }
+ return Optional.empty();
+ }
+
+ public enum Type {
+ AchievementCredential,
+ OpenBadgeCredential, //treated as an alias of AchievementCredential
+ 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("VerifiablePresentation")) {
+ return VerifiablePresentation;
+ } else if(value.equals("EndorsementCredential")) {
+ return EndorsementCredential;
+ }
+ }
+ }
+ return Unknown;
+ }
+ }
+
+ @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();
+
+}
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..a8af2c9
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
@@ -0,0 +1,69 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.core.probe.RunContext.Key.*;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+
+import java.util.Map;
+
+import org.oneedtech.inspect.core.SubInspector;
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+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.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.resource.Resource;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * An inspector for EndersementCredential objects.
+ * @author mgylling
+ */
+public class EndorsementInspector extends VCInspector implements SubInspector {
+
+ protected > EndorsementInspector(B builder) {
+ super(builder);
+ }
+
+ @Override
+ public Report run(Resource resource, Map parentObjects) {
+ /*
+ * resource is the top-level credential that embeds the endorsement, we
+ * expect parentObjects to provide a pointer to the JsonNode we should check
+ */
+ Credential endorsement = (Credential) parentObjects.get(ENDORSEMENT_KEY);
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(resource)
+ .put(JACKSON_OBJECTMAPPER, mapper)
+ .put(JSONPATH_EVALUATOR, jsonPath)
+ .put(ENDORSEMENT_KEY, endorsement)
+ .build();
+
+ System.err.println("TODO" + endorsement.toString());
+
+ return new Report(ctx, new ReportItems(), 1); //TODO
+ }
+
+ @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);
+ }
+ }
+
+ public static final String ENDORSEMENT_KEY = "ENDORSEMENT_KEY";
+
+}
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..8da0e1a
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java
@@ -0,0 +1,195 @@
+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.json.ObjectMapperCache.Config.DEFAULT;
+import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY;
+import static org.oneedtech.inspect.vc.util.JsonNodeUtil.getEndorsements;
+
+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.probe.Outcome;
+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.JsonArrayProbe;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.core.probe.json.JsonPredicates.JsonPredicateProbeParams;
+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.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.probe.CredentialTypeProbe;
+import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe;
+import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe;
+import org.oneedtech.inspect.vc.probe.Predicates;
+import org.oneedtech.inspect.vc.probe.ProofVerifierProbe;
+import org.oneedtech.inspect.vc.probe.RevocationListProbe;
+import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe;
+
+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 {
+ 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
+
+ @Override
+ public Report run(Resource resource) {
+ super.check(resource);
+
+ if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) JsonSchemaCache.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 CredentialTypeProbe().run(resource, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //we expect the above to place a generated object in the context
+ Credential crd = ctx.getGeneratedObject(Credential.ID);
+
+ //validate the value of the type property
+ probeCount++;
+ accumulator.add(new JsonArrayProbe(vcType).run(crd.asJson(), ctx));
+ probeCount++;
+ accumulator.add(new JsonArrayProbe(obType).run(crd.asJson(), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //validate against the canonical schema
+ SchemaKey canonical = crd.getSchemaKey().orElseThrow();
+ probeCount++;
+ accumulator.add(new JsonSchemaProbe(canonical).run(crd.asJson(), ctx));
+
+ //validate against any inline schemas
+ probeCount++;
+ accumulator.add(new InlineJsonSchemaProbe().run(crd, ctx));
+
+ //verify signatures TODO @Miles
+ probeCount++;
+ accumulator.add(new SignatureVerifierProbe().run(crd, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //verify proofs TODO @Miles
+ probeCount++;
+ accumulator.add(new ProofVerifierProbe().run(crd, 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(crd, ctx); //TODO fail = invalid
+ if(newID.isPresent()) {
+ return this.run(
+ new UriResource(new URI(newID.get()))
+ .setContext(new ResourceContext(REFRESHED, TRUE)));
+ }
+ }
+
+ //check revocation status
+ probeCount++;
+ accumulator.add(new RevocationListProbe().run(crd, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //check expiration
+ probeCount++;
+ accumulator.add(new ExpirationVerifierProbe().run(crd, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //check issuance
+ probeCount++;
+ accumulator.add(new IssuanceVerifierProbe().run(crd, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ //embedded endorsements
+ List endorsements = getEndorsements(crd.asJson(), jsonPath);
+ if(endorsements.size() > 0) {
+ EndorsementInspector subInspector = new EndorsementInspector.Builder().build();
+ for(JsonNode endorsementNode : endorsements) {
+ probeCount++;
+ Credential endorsement = new Credential(resource, endorsementNode);
+ accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement)));
+ }
+ }
+
+ //finally, run any user-added probes
+ for(Probe probe : userProbes) {
+ probeCount++;
+ accumulator.add(probe.run(crd, ctx));
+ }
+
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ /**
+ * 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.
+ */
+ private Optional checkRefreshService(Credential crd, RunContext ctx) {
+ //TODO
+ return Optional.empty();
+ }
+
+ private static final String REFRESHED = "is.refreshed.credential";
+
+ private static final JsonPredicateProbeParams obType = JsonPredicateProbeParams.of(
+ "$.type", Predicates.OB30.TypeProperty.value, Predicates.OB30.TypeProperty.msg, Outcome.FATAL);
+
+ private static final JsonPredicateProbeParams vcType = JsonPredicateProbeParams.of(
+ "$.type", Predicates.VC.TypeProperty.value, Predicates.VC.TypeProperty.msg, Outcome.FATAL);
+
+ 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..a07157d
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
@@ -0,0 +1,47 @@
+package org.oneedtech.inspect.vc;
+
+import java.util.ArrayList;
+import java.util.List;
+
+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;
+
+/**
+ * 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) {
+ for(ReportItems items : accumulator) {
+ if(items.contains(Outcome.FATAL, Outcome.EXCEPTION, Outcome.NOT_RUN)) return true;
+ }
+ return false;
+ }
+
+ 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/probe/CredentialTypeProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java
new file mode 100644
index 0000000..0017cbe
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java
@@ -0,0 +1,165 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Base64;
+import java.util.Base64.Decoder;
+import java.util.List;
+import java.util.Optional;
+
+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.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.util.xml.XMLInputFactoryCache;
+import org.oneedtech.inspect.vc.Credential;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+
+/**
+ * A probe that verifies that the incoming credential resource is of a recognized type,
+ * and if so extracts and stores the VC json data (a 'Credential' instance) in the RunContext.
+ * @author mgylling
+ */
+public class CredentialTypeProbe extends Probe {
+
+ @Override
+ public ReportItems run(Resource resource, RunContext context) throws Exception {
+
+ Credential crd = null;
+ try {
+ Optional type = TypeDetector.detect(resource, true);
+
+ if(type.isPresent()) {
+ resource.setType(type.get());
+ if(type.get() == ResourceType.PNG) {
+ crd = new Credential(resource, fromPNG(resource, context));
+ } else if(type.get() == ResourceType.SVG) {
+ crd = new Credential(resource, fromSVG(resource, context));
+ } else if(type.get() == ResourceType.JSON) {
+ crd = new Credential(resource, fromJson(resource, context));
+ } else if(type.get() == ResourceType.JWT) {
+ crd = new Credential(resource, fromJWT(resource, context));
+ }
+ }
+
+ if(crd != null) {
+ context.addGeneratedObject(crd);
+ return success(this, context);
+ } else {
+ return fatal("Could not detect credential type", context);
+ }
+
+ } catch (Exception e) {
+ return fatal("Error while detecting credential type: " + e.getMessage(), context);
+ }
+ }
+
+ /**
+ * Extract the JSON data from a baked PNG credential.
+ * @param context
+ * @throws Exception
+ */
+ private JsonNode fromPNG(Resource resource, RunContext context) throws Exception {
+ //TODO @Miles - note: iTxt chunk is either plain json or jwt
+ try(InputStream is = resource.asByteSource().openStream()) {
+
+ }
+ return null;
+ }
+
+ /**
+ * Extract the JSON data from a baked SVG credential.
+ * @param context
+ * @throws Exception
+ */
+ private JsonNode fromSVG(Resource resource, RunContext context) throws Exception {
+ String json = null;
+ try(InputStream is = resource.asByteSource().openStream()) {
+ XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is);
+ while(reader.hasNext()) {
+ XMLEvent ev = reader.nextEvent();
+ if(ev.isStartElement() && ev.asStartElement().getName().equals(OB_CRED_ELEM)) {
+ Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR);
+ if(verifyAttr != null) {
+ json = decodeJWT(verifyAttr.getValue());
+ break;
+ } else {
+ while(reader.hasNext()) {
+ ev = reader.nextEvent();
+ if(ev.isEndElement() && ev.asEndElement().getName().equals(OB_CRED_ELEM)) {
+ break;
+ }
+ if(ev.getEventType() == XMLEvent.CHARACTERS) {
+ Characters chars = ev.asCharacters();
+ if(!chars.isWhiteSpace()) {
+ json = chars.getData();
+ break;
+ }
+ }
+ }
+ }
+ }
+ if(json!=null) break;
+ }
+ }
+ if(json == null) throw new IllegalArgumentException("No credential inside SVG");
+ return fromString(json, context);
+ }
+
+ /**
+ * Create a JsonNode object from a raw JSON resource.
+ * @param context
+ */
+ private JsonNode fromJson(Resource resource, RunContext context) throws Exception {
+ return fromString(resource.asByteSource().asCharSource(UTF_8).read(), context);
+ }
+
+ /**
+ * Create a JsonNode object from a String.
+ */
+ private JsonNode fromString(String json, RunContext context) throws Exception {
+ return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json);
+ }
+
+ /**
+ * Create a JsonNode object from a JWT resource.
+ * @param context
+ */
+ private JsonNode fromJWT(Resource resource, RunContext context) throws Exception {
+ return fromString(decodeJWT(resource.asByteSource().asCharSource(UTF_8).read()), context);
+ }
+
+ /**
+ * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof
+ * @return The decoded JSON String
+ */
+ private String decodeJWT(String jwt) {
+ 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)));
+ String jwtPayload = new String(decoder.decode(parts.get(1)));
+ String jwsSignature = new String(decoder.decode(parts.get(2)));
+
+ //TODO @Miles
+
+ return null;
+ }
+
+ 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/ExpirationVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationVerifierProbe.java
new file mode 100644
index 0000000..d2d5715
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationVerifierProbe.java
@@ -0,0 +1,47 @@
+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 ExpirationVerifierProbe extends Probe {
+
+ public ExpirationVerifierProbe() {
+ 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.
+ */
+
+ ZonedDateTime now = ZonedDateTime.now();
+ JsonNode node = crd.asJson().get("expirationDate");
+ if(node != null) {
+ ZonedDateTime expirationDate = null;
+ try {
+ expirationDate = ZonedDateTime.parse(node.textValue());
+ if (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 = ExpirationVerifierProbe.class.getSimpleName();
+}
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..01a0e26
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
@@ -0,0 +1,71 @@
+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 final boolean skipCanonical = true;
+
+ public InlineJsonSchemaProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ List accumulator = new ArrayList<>();
+ Set ioErrors = new HashSet<>();
+
+// JsonPathEvaluator jsonPath = ctx.get(RunContext.Key.JSONPATH_EVALUATOR);
+// ArrayNode nodes = jsonPath.eval("$..*[?(@.credentialSchema)]", crd.getJson());
+// note - we dont get deep nested ones in e.g. EndorsementCredential
+
+ JsonNode credentialSchemaNode = crd.asJson().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(skipCanonical && equals(crd.getSchemaKey(), id)) continue;
+ try {
+ accumulator.add(new JsonSchemaProbe(id).run(crd.asJson(), 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(Optional key, String id) {
+ return key.isPresent() && key.get().getCanonicalURI().equals(id);
+ }
+
+ public static final String ID = InlineJsonSchemaProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java
new file mode 100644
index 0000000..53875db
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java
@@ -0,0 +1,47 @@
+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 IssuanceVerifierProbe extends Probe {
+
+ public IssuanceVerifierProbe() {
+ 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.
+ */
+
+ ZonedDateTime now = ZonedDateTime.now();
+ JsonNode node = crd.asJson().get("issuanceDate");
+ if(node != null) {
+ ZonedDateTime issuanceDate = null;
+ try {
+ issuanceDate = ZonedDateTime.parse(node.textValue());
+ if (issuanceDate.isAfter(now)) {
+ return fatal("The credential is not yet valid (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 = IssuanceVerifierProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java
new file mode 100644
index 0000000..e954e36
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java
@@ -0,0 +1,51 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.vc.Credential.Type.*;
+import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asStringList;
+
+import java.util.List;
+import java.util.function.Predicate;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Joiner;
+
+//TODO refactor
+public class Predicates {
+
+ public static class OB30 {
+ public static class TypeProperty {
+ public static final Predicate value = new Predicate<>() {
+ @Override
+ public boolean test(JsonNode node) {
+ List values = asStringList(node);
+ for(String exp : exp) {
+ if(values.contains(exp)) return true;
+ }
+ return false;
+ }
+ };
+ private static final List exp = List.of(OpenBadgeCredential.name(), AchievementCredential.name(), VerifiablePresentation.name());
+ public static final String msg = "The type property does not contain one of " + Joiner.on(", ").join(exp);
+ }
+ }
+
+ public static class VC {
+ public static class TypeProperty {
+ public static final Predicate value = new Predicate<>() {
+ @Override
+ public boolean test(JsonNode node) {
+ List values = asStringList(node);
+ if(values.contains(exp)) return true;
+ return false;
+ }
+ };
+ private static final String exp = VerifiableCredential.name();
+ public static final String msg = "The type property does not contain " + exp;
+ }
+ }
+
+
+
+
+}
+
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java
new file mode 100644
index 0000000..e506aec
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java
@@ -0,0 +1,27 @@
+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 org.oneedtech.inspect.vc.Credential;
+
+/**
+ * A Probe that verifies credential proofs
+ * @author mlyon
+ */
+public class ProofVerifierProbe extends Probe {
+
+ public ProofVerifierProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+
+ //TODO @Miles -- if proofs fail, report OutCome.Fatal
+
+ return success(ctx);
+ }
+
+ public static final String ID = ProofVerifierProbe.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..d491dc6
--- /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.asJson().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.asJson().get("id");
+ 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/SignatureVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java
new file mode 100644
index 0000000..d9961fa
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java
@@ -0,0 +1,28 @@
+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 org.oneedtech.inspect.vc.Credential;
+
+/**
+ * A Probe that verifies credential signatures
+ * @author mlyon
+ */
+public class SignatureVerifierProbe extends Probe {
+
+ public SignatureVerifierProbe() {
+ super(ID);
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+
+ //TODO @Miles -- if sigs fail, report OutCome.Fatal
+
+ return success(ctx);
+ }
+
+ public static final String ID = SignatureVerifierProbe.class.getSimpleName();
+
+}
\ No newline at end of file
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..1d71ed7
--- /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;
+
+/**
+ * Node access utilities.
+ * @author mgylling
+ */
+public class JsonNodeUtil {
+
+ /**
+ * Get all embedded endorsement objects as a flat list.
+ * @return a List that is never null but may be empty.
+ */
+ public static List getEndorsements(JsonNode root, JsonPathEvaluator jsonPath) {
+ List list = new ArrayList<>();
+ ArrayNode endorsements = jsonPath.eval("$..endorsement", root);
+ for(JsonNode endorsement : endorsements) {
+ ArrayNode values = (ArrayNode) endorsement;
+ 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/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..1083aed
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java
@@ -0,0 +1,94 @@
+package org.oneedtech.inspect.vc;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+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.report.Report;
+import org.oneedtech.inspect.test.PrintHelper;
+import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe;
+
+
+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)
+ .build();
+ }
+
+ @Test
+ void testSimpleJsonValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Disabled
+ @Test
+ void testSimplePNGPlainValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Disabled
+ @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);
+ });
+ }
+
+ @Disabled
+ @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() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_UNKNOWN_TYPE.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testCompleteJsonInvalidInlineSchemaRef() throws Exception {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB30.JSON.COMPLETE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertErrorCount(report, 1);
+ 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..2356c2e
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
@@ -0,0 +1,25 @@
+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_UNKNOWN_TYPE = new Sample("ob30/simple-unknown-type.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);
+ }
+ }
+}
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..909cf89
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java
@@ -0,0 +1,30 @@
+package org.oneedtech.inspect.vc.util;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+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;
+
+public class JsonNodeUtilTests {
+ static final ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ static final JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+
+ @Test
+ void getEndorsementsTest() throws Exception {
+ assertDoesNotThrow(()->{
+ JsonNode root = mapper.readTree(Samples.OB30.JSON.COMPLETE_JSON.asBytes());
+ List list = JsonNodeUtil.getEndorsements(root, jsonPath);
+ Assertions.assertEquals(5, list.size());
+ });
+ }
+}
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-json.png b/inspector-vc/src/test/resources/ob30/simple-json.png
new file mode 100644
index 0000000..b424db9
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..90e4219
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-json.svg
@@ -0,0 +1,56 @@
+
+
+
\ 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..47e5afc
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..8fb9094
--- /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-unknown-type.json b/inspector-vc/src/test/resources/ob30/simple-unknown-type.json
new file mode 100644
index 0000000..a54f5c1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple-unknown-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 b/inspector-vc/src/test/resources/ob30/simple.json
new file mode 100644
index 0000000..cdfffdb
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob30/simple.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",
+ "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-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.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