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 @@ + + + + + + { + "@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.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