From 068a6edb8dcf29b44810b9e8a74e0ab610ef59d4 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Mon, 20 Jun 2022 14:51:37 +0200 Subject: [PATCH 01/58] initial cut of ob3 verifier --- inspector-vc/pom.xml | 15 + .../org/oneedtech/inspect/vc/Credential.java | 107 +++ .../inspect/vc/EndorsementInspector.java | 69 ++ .../oneedtech/inspect/vc/OB30Inspector.java | 195 +++++ .../org/oneedtech/inspect/vc/VCInspector.java | 47 ++ .../inspect/vc/probe/CredentialTypeProbe.java | 165 ++++ .../vc/probe/ExpirationVerifierProbe.java | 47 ++ .../vc/probe/InlineJsonSchemaProbe.java | 71 ++ .../vc/probe/IssuanceVerifierProbe.java | 47 ++ .../inspect/vc/probe/Predicates.java | 51 ++ .../inspect/vc/probe/ProofVerifierProbe.java | 27 + .../inspect/vc/probe/RevocationListProbe.java | 79 ++ .../vc/probe/SignatureVerifierProbe.java | 28 + .../inspect/vc/util/JsonNodeUtil.java | 58 ++ .../org/oneedtech/inspect/vc/OB30Tests.java | 94 +++ .../org/oneedtech/inspect/vc/Samples.java | 25 + .../inspect/vc/util/JsonNodeUtilTests.java | 30 + .../src/test/resources/ob30/complete.json | 712 ++++++++++++++++++ .../src/test/resources/ob30/simple-json.png | Bin 0 -> 83758 bytes .../src/test/resources/ob30/simple-json.svg | 56 ++ .../src/test/resources/ob30/simple-jwt.png | Bin 0 -> 83939 bytes .../src/test/resources/ob30/simple-jwt.svg | 17 + .../resources/ob30/simple-unknown-type.json | 36 + .../src/test/resources/ob30/simple.json | 36 + .../src/test/resources/ob30/simple.jwt | 1 + 25 files changed, 2013 insertions(+) create mode 100644 inspector-vc/pom.xml create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationVerifierProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java create mode 100644 inspector-vc/src/test/resources/ob30/complete.json create mode 100644 inspector-vc/src/test/resources/ob30/simple-json.png create mode 100644 inspector-vc/src/test/resources/ob30/simple-json.svg create mode 100644 inspector-vc/src/test/resources/ob30/simple-jwt.png create mode 100644 inspector-vc/src/test/resources/ob30/simple-jwt.svg create mode 100644 inspector-vc/src/test/resources/ob30/simple-unknown-type.json create mode 100644 inspector-vc/src/test/resources/ob30/simple.json create mode 100644 inspector-vc/src/test/resources/ob30/simple.jwt 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 0000000000000000000000000000000000000000..b424db95afd9a5d935c8b9a128c7f78b2b88dd75 GIT binary patch literal 83758 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 + + + + + { + "@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 0000000000000000000000000000000000000000..47e5afc81c95f18cdf6c6f99dcb22a541e147c9b GIT binary patch literal 83939 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 literal 0 HcmV?d00001 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 From c91ae7a38379e8fe07327bde917d88b01db973dd Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Mon, 20 Jun 2022 15:05:27 +0200 Subject: [PATCH 02/58] Update CredentialTypeProbe.java --- .../java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java | 1 - 1 file changed, 1 deletion(-) 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 index 0017cbe..517116e 100644 --- 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 @@ -2,7 +2,6 @@ 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; From 3ff7a0f76a406407c4735e01bed0606d8e45e327 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Mon, 20 Jun 2022 15:13:25 +0200 Subject: [PATCH 03/58] Update OB30Inspector.java --- .../src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 8da0e1a..f2fd220 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -78,7 +78,9 @@ public class OB30Inspector extends VCInspector { List accumulator = new ArrayList<>(); int probeCount = 0; - try { + try { + //TODO turn into a loop once stable + //detect type (png, svg, json, jwt) and extract json data probeCount++; accumulator.add(new CredentialTypeProbe().run(resource, ctx)); From a913ac97d88573a89ff24f353575bca23c9b76b4 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Thu, 23 Jun 2022 11:27:27 -0400 Subject: [PATCH 04/58] Rebaked a couple of badges. Added decode jwt code and png type probe. Refactored accordingly to retain external jwt. Endorsements needs a refactor pending finality on endorsement lookup. --- .../org/oneedtech/inspect/vc/Credential.java | 5 +- .../oneedtech/inspect/vc/OB30Inspector.java | 7 +- .../inspect/vc/probe/CredentialTypeProbe.java | 143 +++++++++++++----- .../org/oneedtech/inspect/vc/OB30Tests.java | 2 - .../src/test/resources/ob30/simple-jwt.png | Bin 83939 -> 84446 bytes .../src/test/resources/ob30/simple-jwt.svg | 2 +- 6 files changed, 117 insertions(+), 42 deletions(-) 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 index 63205c8..4efcf5a 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -25,8 +25,9 @@ public class Credential extends GeneratedObject { final Resource resource; final JsonNode jsonData; final Credential.Type credentialType; + final String jwt; - public Credential(Resource resource, JsonNode data) { + public Credential(Resource resource, JsonNode data, String jwt) { super(ID, GeneratedObject.Type.INTERNAL); checkNotNull(resource, resource.getType(), data); ResourceType type = resource.getType(); @@ -34,10 +35,10 @@ public class Credential extends GeneratedObject { "Unrecognized payload type: " + type.getName()); this.resource = resource; this.jsonData = data; + this.jwt = jwt; ArrayNode typeNode = (ArrayNode)jsonData.get("type"); this.credentialType = Credential.Type.valueOf(typeNode); - } public Resource getResource() { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index f2fd220..71ca34f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -147,8 +147,11 @@ public class OB30Inspector extends VCInspector { 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))); + //TODO: @Markus @Miles, need to refactor to detect as this can be an internal or external proof credential. + //This will LIKELY come from two distinct sources in which case we would detect the type by property name. + //Third param to constructor: Compact JWT -> add third param after decoding. Internal Proof, null jwt string. + //Credential endorsement = new Credential(resource, endorsementNode); + //accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); } } 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 index 517116e..bf59280 100644 --- 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 @@ -2,12 +2,16 @@ package org.oneedtech.inspect.vc.probe; import static java.nio.charset.StandardCharsets.UTF_8; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.Base64.Decoder; import java.util.List; import java.util.Optional; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.events.Attribute; @@ -22,6 +26,8 @@ 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 org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -43,14 +49,19 @@ public class CredentialTypeProbe extends Probe { if(type.isPresent()) { resource.setType(type.get()); + //TODO: Refactor to return the entire credential so we can include optional encoded JWT. if(type.get() == ResourceType.PNG) { - crd = new Credential(resource, fromPNG(resource, context)); + //crd = new Credential(resource, fromPNG(resource, context)); + crd = fromPNG(resource, context); } else if(type.get() == ResourceType.SVG) { - crd = new Credential(resource, fromSVG(resource, context)); + //crd = new Credential(resource, fromSVG(resource, context)); + crd = fromSVG(resource, context); } else if(type.get() == ResourceType.JSON) { - crd = new Credential(resource, fromJson(resource, context)); + //crd = new Credential(resource, fromJson(resource, context)); + crd = fromJson(resource, context); } else if(type.get() == ResourceType.JWT) { - crd = new Credential(resource, fromJWT(resource, context)); + //crd = new Credential(resource, fromJWT(resource, context)); + crd = fromJWT(resource, context); } } @@ -71,12 +82,39 @@ public class CredentialTypeProbe extends Probe { * @param context * @throws Exception */ - private JsonNode fromPNG(Resource resource, RunContext context) throws Exception { - //TODO @Miles - note: iTxt chunk is either plain json or jwt + private Credential fromPNG(Resource resource, RunContext context) throws Exception { try(InputStream is = resource.asByteSource().openStream()) { + ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); + imageReader.setInput(ImageIO.createImageInputStream(is), true); + IIOMetadata metadata = imageReader.getImageMetadata(0); + + String credentialString = null; + String jwtString = null; + String formatSearch = null; + JsonNode credential = null; + String[] names = metadata.getMetadataFormatNames(); + int length = names.length; + for (int i = 0; i < length; i++) + { + //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0") + formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i])); + if(formatSearch != null) { credentialString = formatSearch; } + } + + if(credentialString == null) { throw new IllegalArgumentException("No credential inside PNG"); } + + credentialString = credentialString.trim(); + if(credentialString.charAt(0) != '{'){ + //This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification. + jwtString = credentialString; + credential = decodeJWT(credentialString,context); + } + else { + credential = buildNodeFromString(credentialString, context); + } + return new Credential(resource, credential, jwtString); } - return null; } /** @@ -84,8 +122,10 @@ public class CredentialTypeProbe extends Probe { * @param context * @throws Exception */ - private JsonNode fromSVG(Resource resource, RunContext context) throws Exception { + private Credential fromSVG(Resource resource, RunContext context) throws Exception { String json = null; + String jwtString = null; + JsonNode credential = null;; try(InputStream is = resource.asByteSource().openStream()) { XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is); while(reader.hasNext()) { @@ -93,7 +133,8 @@ public class CredentialTypeProbe extends Probe { 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()); + jwtString = verifyAttr.getValue(); + credential = decodeJWT(verifyAttr.getValue(), context); break; } else { while(reader.hasNext()) { @@ -105,58 +146,90 @@ public class CredentialTypeProbe extends Probe { Characters chars = ev.asCharacters(); if(!chars.isWhiteSpace()) { json = chars.getData(); + credential = buildNodeFromString(json, context); break; } } - } - } - } - if(json!=null) break; + } + } + } + if(credential!=null) break; } } - if(json == null) throw new IllegalArgumentException("No credential inside SVG"); - return fromString(json, context); + if(credential == null) throw new IllegalArgumentException("No credential inside SVG"); + return new Credential(resource, credential, jwtString); } /** - * Create a JsonNode object from a raw JSON resource. + * Create a Credential 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); + private Credential fromJson(Resource resource, RunContext context) throws Exception { + return new Credential(resource, buildNodeFromString(resource.asByteSource().asCharSource(UTF_8).read(), context), null); } - + + /** + * Create a Credential object from a JWT resource. + * @param context + */ + private Credential fromJWT(Resource resource, RunContext context) throws Exception { + return new Credential( + resource, + decodeJWT( + resource.asByteSource().asCharSource(UTF_8).read(), + context + ) + , resource.asByteSource().asCharSource(UTF_8).read() + ); + } + /** * Create a JsonNode object from a String. */ - private JsonNode fromString(String json, RunContext context) throws Exception { + private JsonNode buildNodeFromString(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) { + private JsonNode decodeJWT(String jwt, RunContext context) throws Exception { List parts = Splitter.on('.').splitToList(jwt); if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); final Decoder decoder = Base64.getUrlDecoder(); - String joseHeader = new String(decoder.decode(parts.get(0))); + //For this step we are only deserializing the stored badge out of the payload. The entire jwt is stored separately for + //signature verification later. String jwtPayload = new String(decoder.decode(parts.get(1))); - String jwsSignature = new String(decoder.decode(parts.get(2))); - - //TODO @Miles - - return null; + + //Deserialize and fetch the 'vc' node from the object + JsonNode outerPayload = buildNodeFromString(jwtPayload, context); + JsonNode vcNode = outerPayload.get("vc"); + + return vcNode; + } + + private String getOpenBadgeCredentialNodeText(Node node){ + NamedNodeMap attributes = node.getAttributes(); + + //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one. + if(attributes.getNamedItem("keyword") != null && attributes.getNamedItem("keyword").getNodeValue().equals("openbadgecredential")){ + Node textAttribute = attributes.getNamedItem("text"); + if(textAttribute != null) { return textAttribute.getNodeValue(); } + } + + //iterate over all children depth first and search for the credential node. + Node child = node.getFirstChild(); + while (child != null) + { + String nodeValue = getOpenBadgeCredentialNodeText(child); + if(nodeValue != null) { return nodeValue; } + child = child.getNextSibling(); + } + + //Return null if we haven't found anything at this recursive depth. + return null; } private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential"); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 1083aed..4426250 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -32,7 +32,6 @@ public class OB30Tests { }); } - @Disabled @Test void testSimplePNGPlainValid() { assertDoesNotThrow(()->{ @@ -42,7 +41,6 @@ public class OB30Tests { }); } - @Disabled @Test void testSimplePNGJWTValid() { assertDoesNotThrow(()->{ diff --git a/inspector-vc/src/test/resources/ob30/simple-jwt.png b/inspector-vc/src/test/resources/ob30/simple-jwt.png index 47e5afc81c95f18cdf6c6f99dcb22a541e147c9b..84a9ab2fa642d0a39d8779b00ce66e1bfbd1505f 100644 GIT binary patch delta 900 zcmWkpNv@+t0L@RUv&aSd6kjU9V6*6!j2R4OFhx}{Y#c*jV<;x$ZyPp_a)DNqMQ)K* zOF2o@yKJ(`Iv0rh?cUSV8~*wH{_o%3|NQ0UFXj6m{`>u_^783n4rbj6`_Yv0(PC!V zo_wM!{^^^30eN6bCbN(qeB-l|cd|MMvC?6X1z{%!rM2?IM9L7Ytn+Q_NXSRvqt2lk~LF6MW(>gcZ6U z>2R-O9viY9GUd%NtQKN_hAWOG%mFb<4lZvs385WDR7mR%ph;&gPhvZQcNgn7f68XL zK@A+rT3#0%j^J|)Sv#d-J@yQ?ke7$hU^k>u+o>-tDz(wZ?L8*X#n|1)q!oQ2V^oP# zqPju+{78*Lp{grU)v_ZX55a_Q1Q0W>h)622r!1Q?lEgj-MFQN2axoXyu0U1YiG=0l z=`}FV9swlBTMF3ORTK0q0;yVoOmKxTsz89qH4iUMPKwUO_c+y(5)ln}1bRu;U9lS9 zAdz`jRgAECenv`()_NUOg@;kGf99?UpLAIW_Zjb2K3I%dWZ4LlF(umR5WRpuKdfF3 z?;hC5WUt)cpkdrMq_t$~JDbHxR2}d!>)GG+K zmE4v_IbFv2mh(0WOqKPrUrTRw92iA)M?!z zyTFYU&2BgUtZVtwT$*pnYwxl7n(V{t>8{_IY8B5rXf zZnWk#)0ej3U8x^3@xYFX>p8=CXGPa1!d$6_z|jMowXxcx{Ipm}zQGk7KU<&qB3oWU zhxF2vhDCAa1>kAc6(bkd0ZYYsC{g6FeHUH>|)!L&T%Z76?@X1k~G1J`g~)Zj1g^(+%%^;9grA0nD%;0V*7d z&PI^rht?4oaL%4H?-yRtp>u+OR_a>~W@9bYp - + From 98e00ccf4890d0ce4f5465a10bfe6c16ccf9df98 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:16:28 -0400 Subject: [PATCH 05/58] WIP, signature additions. --- inspector-vc/pom.xml | 25 +++++++ .../org/oneedtech/inspect/vc/Credential.java | 4 ++ .../oneedtech/inspect/vc/OB30Inspector.java | 11 +-- .../inspect/vc/probe/CredentialTypeProbe.java | 5 -- .../vc/probe/SignatureVerifierProbe.java | 70 ++++++++++++++++++- 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 6881751..396f3d5 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -10,6 +10,31 @@ org.1edtech inspector-core + + + org.bouncycastle + bcpkix-jdk15on + 1.58 + + + com.auth0 + auth0 + 1.20.0 + + + com.auth0 + jwks-rsa + 0.12.0 + + + com.auth0 + java-jwt + 3.10.3 + + + org.springframework + spring-core + 5.0.12.RELEASE \ 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 index 4efcf5a..3e73655 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -52,6 +52,10 @@ public class Credential extends GeneratedObject { public Credential.Type getCredentialType() { return credentialType; } + + public String getJwt() { + return jwt; + } /** * Get the canonical schema for this credential if such exists. diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 71ca34f..da95e80 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -6,6 +6,7 @@ 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 static com.google.common.base.Strings.isNullOrEmpty; import java.net.URI; import java.util.ArrayList; @@ -105,10 +106,12 @@ public class OB30Inspector extends VCInspector { 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); + //If this credential was originally contained in a JWT we must validate the jwt and external proof. + if(!isNullOrEmpty(crd.getJwt())){ + probeCount++; + accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } //verify proofs TODO @Miles probeCount++; 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 index bf59280..535c9c0 100644 --- 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 @@ -49,18 +49,13 @@ public class CredentialTypeProbe extends Probe { if(type.isPresent()) { resource.setType(type.get()); - //TODO: Refactor to return the entire credential so we can include optional encoded JWT. if(type.get() == ResourceType.PNG) { - //crd = new Credential(resource, fromPNG(resource, context)); crd = fromPNG(resource, context); } else if(type.get() == ResourceType.SVG) { - //crd = new Credential(resource, fromSVG(resource, context)); crd = fromSVG(resource, context); } else if(type.get() == ResourceType.JSON) { - //crd = new Credential(resource, fromJson(resource, context)); crd = fromJson(resource, context); } else if(type.get() == ResourceType.JWT) { - //crd = new Credential(resource, fromJWT(resource, context)); crd = fromJWT(resource, context); } } 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 index d9961fa..08ad567 100644 --- 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 @@ -1,9 +1,30 @@ package org.oneedtech.inspect.vc.probe; +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.List; +import java.util.Base64; +import java.util.Base64.Decoder; + 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.springframework.util.Base64Utils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Splitter; /** * A Probe that verifies credential signatures @@ -17,11 +38,56 @@ public class SignatureVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { - - //TODO @Miles -- if sigs fail, report OutCome.Fatal + try { + testingSignatureValidationCode(crd); + } catch (Exception e) { + return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); + } return success(ctx); } + + private void testingSignatureValidationCode(Credential crd) throws Exception { + DecodedJWT decodedJwt = null; + String jwt = crd.getJwt(); + if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid 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))); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode headerObj = mapper.readTree(joseHeader); + + //MUST be "RS256" + + //Option 1, fetch directly from header + JsonNode jwk = headerObj.get("jwk"); + + //Option 2, fetch from a hosting url + JsonNode kid = headerObj.get("kid"); + + if(jwk == null && kid == null) { throw new Exception("asdf"); } + if(kid != null){ + //TODO @Miles load jwk JsonNode from url and do the rest the same below. Need to set up testing. + String kidUrl = kid.textValue(); + } + + //Clean up may be required. Currently need to cleanse extra double quoting. + String modulusString = jwk.get("n").textValue(); + String exponentString = jwk.get("e").textValue(); + + BigInteger modulus = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(modulusString)); + BigInteger exponent = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(exponentString)); + + PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent)); + + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null); + JWTVerifier verifier = JWT.require(algorithm) + .build(); //Reusable verifier instance + decodedJwt = verifier.verify(jwt); + } public static final String ID = SignatureVerifierProbe.class.getSimpleName(); From 78456fceb81d34ce9b7dbcdbf5f55a876ce36cc7 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:23:30 -0400 Subject: [PATCH 06/58] Added 'alg' check. --- .../oneedtech/inspect/vc/probe/SignatureVerifierProbe.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index 08ad567..91f4f64 100644 --- 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 @@ -61,6 +61,10 @@ public class SignatureVerifierProbe extends Probe { JsonNode headerObj = mapper.readTree(joseHeader); //MUST be "RS256" + JsonNode alg = headerObj.get("alg"); + if(alg == null || !alg.textValue().equals("RS256")) { throw new Exception("alg must be present and must be 'RS256'"); } + + //TODO: decoded jwt will check timestamps, but shall we explicitly break these out? //Option 1, fetch directly from header JsonNode jwk = headerObj.get("jwk"); @@ -68,7 +72,7 @@ public class SignatureVerifierProbe extends Probe { //Option 2, fetch from a hosting url JsonNode kid = headerObj.get("kid"); - if(jwk == null && kid == null) { throw new Exception("asdf"); } + if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); } if(kid != null){ //TODO @Miles load jwk JsonNode from url and do the rest the same below. Need to set up testing. String kidUrl = kid.textValue(); From c2540377e2cdf77ef4096d3228e8a4319a118bb9 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:45:27 -0400 Subject: [PATCH 07/58] Updated bouncy castle to a higher version to support ED25529. Pulled in Stack Overflow signature/proof verification. --- inspector-vc/pom.xml | 6 ++--- .../oneedtech/inspect/vc/OB30Inspector.java | 9 ++++--- .../inspect/vc/probe/ProofVerifierProbe.java | 27 +++++++++++++++++++ .../vc/probe/SignatureVerifierProbe.java | 24 ++++++++++++++--- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 396f3d5..f253160 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -11,10 +11,10 @@ org.1edtech inspector-core - + org.bouncycastle - bcpkix-jdk15on - 1.58 + bcprov-jdk15to18 + 1.65 com.auth0 diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index da95e80..d68c7e8 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -114,9 +114,12 @@ public class OB30Inspector extends VCInspector { } //verify proofs TODO @Miles - probeCount++; - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + //If this credential was not contained in a jwt it must have an internal proof. + if(isNullOrEmpty(crd.getJwt())){ + 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++; 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 index e506aec..1618148 100644 --- 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 @@ -1,5 +1,15 @@ package org.oneedtech.inspect.vc.probe; +import java.security.KeyFactory; +import java.security.Security; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.encoders.Hex; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; @@ -23,5 +33,22 @@ public class ProofVerifierProbe extends Probe { return success(ctx); } + public boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { + //TODO: continue this implementation. + //Pulled in bouncy castle library and made sure this sample compiled only. + final var provider = new BouncyCastleProvider(); + Security.addProvider(provider); + final var byteKey = Hex.decode(pubkey); + final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), byteKey); + final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); + final var kf = KeyFactory.getInstance("ed25519", provider); + final var publicKey = kf.generatePublic(pkSpec); + final var signedData = Signature.getInstance("ed25519", provider); + signedData.initVerify(publicKey); + signedData.update(timestamp.getBytes()); + signedData.update(message.getBytes()); + return signedData.verify(Hex.decode(signature)); + } + public static final String ID = ProofVerifierProbe.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 index 91f4f64..e8c06ae 100644 --- 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 @@ -21,6 +21,10 @@ import org.springframework.util.Base64Utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.AlgorithmMismatchException; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,7 +43,7 @@ public class SignatureVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { try { - testingSignatureValidationCode(crd); + verifySignature(crd); } catch (Exception e) { return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); } @@ -47,7 +51,7 @@ public class SignatureVerifierProbe extends Probe { return success(ctx); } - private void testingSignatureValidationCode(Credential crd) throws Exception { + private void verifySignature(Credential crd) throws Exception { DecodedJWT decodedJwt = null; String jwt = crd.getJwt(); if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid jwt"); @@ -90,7 +94,21 @@ public class SignatureVerifierProbe extends Probe { Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null); JWTVerifier verifier = JWT.require(algorithm) .build(); //Reusable verifier instance - decodedJwt = verifier.verify(jwt); + try { + decodedJwt = verifier.verify(jwt); + } + catch(SignatureVerificationException ex){ + throw new Exception("JWT Invalid signature", ex); + } + catch(AlgorithmMismatchException ex){ + throw new Exception("JWT Algorithm mismatch", ex); + } + catch(TokenExpiredException ex){ + throw new Exception("JWT Token expired", ex); + } + catch(InvalidClaimException ex){ + throw new Exception("JWT, one or more claims are invalid", ex); + } } public static final String ID = SignatureVerifierProbe.class.getSimpleName(); From e09a3830975d90806607add41b1bc401971926ce Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Wed, 29 Jun 2022 16:08:18 -0400 Subject: [PATCH 08/58] One additional commit which is just to update our simple.json proof with a sample with a key directly in the file. --- .../src/test/resources/ob30/simple.json | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/inspector-vc/src/test/resources/ob30/simple.json b/inspector-vc/src/test/resources/ob30/simple.json index cdfffdb..50e33aa 100644 --- a/inspector-vc/src/test/resources/ob30/simple.json +++ b/inspector-vc/src/test/resources/ob30/simple.json @@ -1,36 +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", + "@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": [ - "VerifiableCredential", - "OpenBadgeCredential" + "Profile" ], - "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" - } + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" ] - } \ No newline at end of file + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file From b32e81bed5621fb57dec59f23fed47e092c4ddaa Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Wed, 29 Jun 2022 16:09:35 -0400 Subject: [PATCH 09/58] Leaving the old version in case it is needed. We can delete later. --- .../resources/ob30/simple-old-delete.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 inspector-vc/src/test/resources/ob30/simple-old-delete.json diff --git a/inspector-vc/src/test/resources/ob30/simple-old-delete.json b/inspector-vc/src/test/resources/ob30/simple-old-delete.json new file mode 100644 index 0000000..7795327 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-old-delete.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 From 5d5f3f7588548a3b8dd2b3eba762ad9f66bd642c Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 29 Jun 2022 23:00:37 +0200 Subject: [PATCH 10/58] add URDNA2015 --- inspector-vc/pom.xml | 30 +++++++++- .../inspect/vc/probe/ProofVerifierProbe.java | 60 +++++++++++++++++-- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index f253160..3e4ca11 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -35,6 +35,34 @@ org.springframework spring-core 5.0.12.RELEASE - + + + + com.apicatalog + titanium-json-ld + 1.3.1 + + + + + io.setl + rdf-urdna + 1.1 + + + junit + junit + + + com.apicatalog + titanium-json-ld + + + + + org.glassfish + jakarta.json + 2.0.1 + \ No newline at end of file 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 index 1618148..e3073aa 100644 --- 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 @@ -1,5 +1,9 @@ package org.oneedtech.inspect.vc.probe; +import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPPER; + +import java.io.ByteArrayOutputStream; +import java.io.StringReader; import java.security.KeyFactory; import java.security.Security; import java.security.Signature; @@ -15,6 +19,18 @@ import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; +import com.apicatalog.jsonld.JsonLd; +import com.apicatalog.jsonld.StringUtils; +import com.apicatalog.jsonld.document.JsonDocument; +import com.apicatalog.jsonld.http.media.MediaType; +import com.apicatalog.rdf.Rdf; +import com.apicatalog.rdf.RdfDataset; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import io.setl.rdf.normalization.RdfNormalize; + /** * A Probe that verifies credential proofs * @author mlyon @@ -27,13 +43,45 @@ public class ProofVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { - - //TODO @Miles -- if proofs fail, report OutCome.Fatal - + + try { + String canonical = canonicalize(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); + //System.out.println(canonical); + + //TODO if proofs fail, report OutCome.Fatal + //return fatal("msg", ctx); + + } catch (Exception e) { + return exception(e.getMessage(), crd.getResource()); + } return success(ctx); } - public boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { + private String canonicalize(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { + + //clone the incoming credential object so we can modify it freely + ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); + JsonNode copy = mapper.readTree(crd.asJson().toString()); + + //remove proof + ((ObjectNode)copy).remove("proof"); + + //create JSON-P Json-LD instance + JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(copy.toString())); + + //create rdf and normalize + RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); + RdfDataset normalized = RdfNormalize.normalize(dataSet); + + //serialize to string + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Rdf.createWriter(mediaType, os).write(normalized); + String result = StringUtils.stripTrailing(os.toString()); + + return result; + } + + private boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { //TODO: continue this implementation. //Pulled in bouncy castle library and made sure this sample compiled only. final var provider = new BouncyCastleProvider(); @@ -49,6 +97,10 @@ public class ProofVerifierProbe extends Probe { signedData.update(message.getBytes()); return signedData.verify(Hex.decode(signature)); } + + private enum C14n { + URDNA2015 + } public static final String ID = ProofVerifierProbe.class.getSimpleName(); } From c655b3ca1c8d821f3534119719ffa2c947299bbe Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 30 Jun 2022 15:13:46 +0200 Subject: [PATCH 11/58] misc small edits --- .../java/org/oneedtech/inspect/vc/Credential.java | 2 +- .../oneedtech/inspect/vc/EndorsementInspector.java | 5 ++--- .../java/org/oneedtech/inspect/vc/OB30Inspector.java | 12 +++++------- .../inspect/vc/probe/ExpirationVerifierProbe.java | 2 +- .../inspect/vc/probe/InlineJsonSchemaProbe.java | 4 ++-- .../inspect/vc/probe/IssuanceVerifierProbe.java | 2 +- .../inspect/vc/probe/ProofVerifierProbe.java | 4 ++-- .../inspect/vc/probe/RevocationListProbe.java | 4 ++-- 8 files changed, 16 insertions(+), 19 deletions(-) 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 index 3e73655..dce7153 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -45,7 +45,7 @@ public class Credential extends GeneratedObject { return resource; } - public JsonNode asJson() { + public JsonNode getJson() { return jsonData; } 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 index a8af2c9..37c84af 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -8,7 +8,6 @@ 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; @@ -18,7 +17,7 @@ import org.oneedtech.inspect.util.resource.Resource; import com.fasterxml.jackson.databind.ObjectMapper; /** - * An inspector for EndersementCredential objects. + * An inspector for EndorsementCredential objects. * @author mgylling */ public class EndorsementInspector extends VCInspector implements SubInspector { @@ -30,7 +29,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector { @Override public Report run(Resource resource, Map parentObjects) { /* - * resource is the top-level credential that embeds the endorsement, we + * The resource param is the top-level credential that embeds the endorsement, we * expect parentObjects to provide a pointer to the JsonNode we should check */ Credential endorsement = (Credential) parentObjects.get(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 index d68c7e8..c1849f4 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -1,17 +1,15 @@ package org.oneedtech.inspect.vc; +import static com.google.common.base.Strings.isNullOrEmpty; 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 static com.google.common.base.Strings.isNullOrEmpty; 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; @@ -92,15 +90,15 @@ public class OB30Inspector extends VCInspector { //validate the value of the type property probeCount++; - accumulator.add(new JsonArrayProbe(vcType).run(crd.asJson(), ctx)); + accumulator.add(new JsonArrayProbe(vcType).run(crd.getJson(), ctx)); probeCount++; - accumulator.add(new JsonArrayProbe(obType).run(crd.asJson(), ctx)); + accumulator.add(new JsonArrayProbe(obType).run(crd.getJson(), 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)); + accumulator.add(new JsonSchemaProbe(canonical).run(crd.getJson(), ctx)); //validate against any inline schemas probeCount++; @@ -148,7 +146,7 @@ public class OB30Inspector extends VCInspector { if(broken(accumulator)) return abort(ctx, accumulator, probeCount); //embedded endorsements - List endorsements = getEndorsements(crd.asJson(), jsonPath); + List endorsements = getEndorsements(crd.getJson(), jsonPath); if(endorsements.size() > 0) { EndorsementInspector subInspector = new EndorsementInspector.Builder().build(); for(JsonNode endorsementNode : endorsements) { 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 index d2d5715..e511902 100644 --- 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 @@ -28,7 +28,7 @@ public class ExpirationVerifierProbe extends Probe { */ ZonedDateTime now = ZonedDateTime.now(); - JsonNode node = crd.asJson().get("expirationDate"); + JsonNode node = crd.getJson().get("expirationDate"); if(node != null) { ZonedDateTime expirationDate = null; try { 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 index 01a0e26..94f6363 100644 --- 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 @@ -37,7 +37,7 @@ public class InlineJsonSchemaProbe extends Probe { // ArrayNode nodes = jsonPath.eval("$..*[?(@.credentialSchema)]", crd.getJson()); // note - we dont get deep nested ones in e.g. EndorsementCredential - JsonNode credentialSchemaNode = crd.asJson().get("credentialSchema"); + JsonNode credentialSchemaNode = crd.getJson().get("credentialSchema"); if(credentialSchemaNode == null) return success(ctx); ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast @@ -51,7 +51,7 @@ public class InlineJsonSchemaProbe extends Probe { if(ioErrors.contains(id)) continue; if(skipCanonical && equals(crd.getSchemaKey(), id)) continue; try { - accumulator.add(new JsonSchemaProbe(id).run(crd.asJson(), ctx)); + accumulator.add(new JsonSchemaProbe(id).run(crd.getJson(), ctx)); } catch (Exception e) { if(!ioErrors.contains(id)) { ioErrors.add(id); 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 index 53875db..7eef25a 100644 --- 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 @@ -28,7 +28,7 @@ public class IssuanceVerifierProbe extends Probe { */ ZonedDateTime now = ZonedDateTime.now(); - JsonNode node = crd.asJson().get("issuanceDate"); + JsonNode node = crd.getJson().get("issuanceDate"); if(node != null) { ZonedDateTime issuanceDate = null; try { 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 index e3073aa..a95b9f8 100644 --- 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 @@ -61,7 +61,7 @@ public class ProofVerifierProbe extends Probe { //clone the incoming credential object so we can modify it freely ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); - JsonNode copy = mapper.readTree(crd.asJson().toString()); + JsonNode copy = mapper.readTree(crd.getJson().toString()); //remove proof ((ObjectNode)copy).remove("proof"); @@ -69,7 +69,7 @@ public class ProofVerifierProbe extends Probe { //create JSON-P Json-LD instance JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(copy.toString())); - //create rdf and normalize + //create rdf and normalize //TODO add DocumentLoader to cache contexts RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); RdfDataset normalized = RdfNormalize.normalize(dataSet); 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 index d491dc6..ca11b0f 100644 --- 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 @@ -36,7 +36,7 @@ public class RevocationListProbe extends Probe { * report a warning, not an error. */ - JsonNode credentialStatus = crd.asJson().get("credentialStatus"); + JsonNode credentialStatus = crd.getJson().get("credentialStatus"); if(credentialStatus != null) { JsonNode type = credentialStatus.get("type"); if(type != null && type.asText().strip().equals("1EdTechRevocationList")) { @@ -52,7 +52,7 @@ public class RevocationListProbe extends Probe { * 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"); + JsonNode crdID = crd.getJson().get("id"); //TODO these != checks sb removed (trigger warning) if(crdID != null) { List list = JsonNodeUtil.asNodeList(revocList.get("revokedCredentials")); if(list != null) { From 80e7f9bbde2448a896431e34ba5b792674b37716 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 30 Jun 2022 15:55:17 +0200 Subject: [PATCH 12/58] implement refreshService --- .../org/oneedtech/inspect/vc/OB30Inspector.java | 16 +++++++++++++--- .../inspect/vc/probe/CredentialTypeProbe.java | 1 - 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index c1849f4..88f007e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -22,6 +22,7 @@ import org.oneedtech.inspect.core.probe.json.JsonPredicates.JsonPredicateProbePa 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.core.report.ReportUtil; import org.oneedtech.inspect.schema.JsonSchemaCache; import org.oneedtech.inspect.schema.SchemaKey; import org.oneedtech.inspect.util.json.ObjectMapperCache; @@ -121,8 +122,8 @@ public class OB30Inspector extends VCInspector { //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(resource.getContext().get(REFRESHED) != TRUE) { + Optional newID = checkRefreshService(crd, ctx); if(newID.isPresent()) { return this.run( new UriResource(new URI(newID.get())) @@ -179,7 +180,16 @@ public class OB30Inspector extends VCInspector { * the credential is invalid. */ private Optional checkRefreshService(Credential crd, RunContext ctx) { - //TODO + JsonNode refreshServiceNode = crd.getJson().get("refreshService"); + if(refreshServiceNode != null) { + JsonNode serviceTypeNode = refreshServiceNode.get("type"); + if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { + JsonNode serviceURINode = refreshServiceNode.get("id"); + if(serviceURINode != null) { + return Optional.of(serviceURINode.asText()); + } + } + } return Optional.empty(); } 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 index 535c9c0..1995c0d 100644 --- 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 @@ -2,7 +2,6 @@ package org.oneedtech.inspect.vc.probe; import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.Base64.Decoder; From ebcd653faeb28e74fef0375f0d7f2f55b120745f Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Tue, 5 Jul 2022 23:26:05 +0200 Subject: [PATCH 13/58] add a caching document loader --- .../oneedtech/inspect/vc/OB30Inspector.java | 1 - .../inspect/vc/probe/CredentialTypeProbe.java | 3 + .../vc/util/CachingDocumentLoader.java | 64 +++ .../2018-credentials-examples-v1.jsonld | 69 +++ .../contexts/2018-credentials-v1.jsonld | 315 +++++++++++++ .../src/main/resources/contexts/did-v1.jsonld | 58 +++ .../src/main/resources/contexts/obv3.jsonld | 440 ++++++++++++++++++ .../src/main/resources/contexts/odrl.jsonld | 301 ++++++++++++ .../security-suites-ed25519-2020-v1.jsonld | 93 ++++ .../vc/util/CachingDocumentLoaderTests.java | 24 + 10 files changed, 1367 insertions(+), 1 deletion(-) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java create mode 100644 inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/did-v1.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/obv3.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/odrl.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 88f007e..16de1ae 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -22,7 +22,6 @@ import org.oneedtech.inspect.core.probe.json.JsonPredicates.JsonPredicateProbePa 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.core.report.ReportUtil; import org.oneedtech.inspect.schema.JsonSchemaCache; import org.oneedtech.inspect.schema.SchemaKey; import org.oneedtech.inspect.util.json.ObjectMapperCache; 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 index 1995c0d..1634a82 100644 --- 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 @@ -44,6 +44,9 @@ public class CredentialTypeProbe extends Probe { Credential crd = null; try { + + //TODO: this reads from a URIResource twice. Cache the resource on first call. + Optional type = TypeDetector.detect(resource, true); if(type.isPresent()) { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java new file mode 100644 index 0000000..7032fe2 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -0,0 +1,64 @@ +package org.oneedtech.inspect.vc.util; + +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.time.Duration; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.oneedtech.inspect.util.code.Closeables; +import org.oneedtech.inspect.util.code.Tuple; + +import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.document.Document; +import com.apicatalog.jsonld.document.JsonDocument; +import com.apicatalog.jsonld.loader.DocumentLoader; +import com.apicatalog.jsonld.loader.DocumentLoaderOptions; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; + +/** + * A DocumentLoader with a threadsafe static cache. + * @author mgylling + */ +public class CachingDocumentLoader implements DocumentLoader { + + @Override + public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { + Tuple tpl = new Tuple<>(url.toASCIIString(), options); + try { + return documentCache.get(tpl); + } catch (Exception e) { + logger.error("contextCache not able to load {}", url); + } + return null; + } + + private static final ImmutableMap bundled = ImmutableMap.builder() + .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) + .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) + .put("https://w3id.org/security/suites/ed25519-2020/v1", Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) + .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) + .put("https://imsglobal.github.io/openbadges-specification/context.json", Resources.getResource("contexts/obv3.jsonld")) + .build(); + + private static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder() + .initialCapacity(32) + .maximumSize(64) + .expireAfterAccess(Duration.ofHours(24)) + .build(new CacheLoader, Document>() { + public Document load(final Tuple id) throws Exception { + try (InputStream is = bundled.keySet().contains(id.t1) + ? bundled.get(id.t1).openStream() + : new URI(id.t1).toURL().openStream();) { + return JsonDocument.of(is); + } + } + }); + + private static final Logger logger = LogManager.getLogger(); +} diff --git a/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld b/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld new file mode 100644 index 0000000..425199c --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/2018-credentials-examples-v1.jsonld @@ -0,0 +1,69 @@ +{ + "@context": [ + { + "@version": 1.1 + }, + "https://www.w3.org/ns/odrl.jsonld", + { + "ex": "https://example.org/examples#", + "schema": "http://schema.org/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "3rdPartyCorrelation": "ex:3rdPartyCorrelation", + "AllVerifiers": "ex:AllVerifiers", + "Archival": "ex:Archival", + "BachelorDegree": "ex:BachelorDegree", + "Child": "ex:Child", + "CLCredentialDefinition2019": "ex:CLCredentialDefinition2019", + "CLSignature2019": "ex:CLSignature2019", + "IssuerPolicy": "ex:IssuerPolicy", + "HolderPolicy": "ex:HolderPolicy", + "Mother": "ex:Mother", + "RelationshipCredential": "ex:RelationshipCredential", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential", + "AlumniCredential": "ex:AlumniCredential", + "DisputeCredential": "ex:DisputeCredential", + "PrescriptionCredential": "ex:PrescriptionCredential", + "ZkpExampleSchema2018": "ex:ZkpExampleSchema2018", + "issuerData": "ex:issuerData", + "attributes": "ex:attributes", + "signature": "ex:signature", + "signatureCorrectnessProof": "ex:signatureCorrectnessProof", + "primaryProof": "ex:primaryProof", + "nonRevocationProof": "ex:nonRevocationProof", + "alumniOf": { + "@id": "schema:alumniOf", + "@type": "rdf:HTML" + }, + "child": { + "@id": "ex:child", + "@type": "@id" + }, + "degree": "ex:degree", + "degreeType": "ex:degreeType", + "degreeSchool": "ex:degreeSchool", + "college": "ex:college", + "name": { + "@id": "schema:name", + "@type": "rdf:HTML" + }, + "givenName": "schema:givenName", + "familyName": "schema:familyName", + "parent": { + "@id": "ex:parent", + "@type": "@id" + }, + "referenceId": "ex:referenceId", + "documentPresence": "ex:documentPresence", + "evidenceDocument": "ex:evidenceDocument", + "spouse": "schema:spouse", + "subjectPresence": "ex:subjectPresence", + "verifier": { + "@id": "ex:verifier", + "@type": "@id" + }, + "currentStatus": "ex:currentStatus", + "statusReason": "ex:statusReason", + "prescription": "ex:prescription" + } + ] +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld b/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld new file mode 100644 index 0000000..837104f --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/2018-credentials-v1.jsonld @@ -0,0 +1,315 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": { + "@id": "cred:credentialStatus", + "@type": "@id" + }, + "credentialSubject": { + "@id": "cred:credentialSubject", + "@type": "@id" + }, + "evidence": { + "@id": "cred:evidence", + "@type": "@id" + }, + "expirationDate": { + "@id": "cred:expirationDate", + "@type": "xsd:dateTime" + }, + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "issued": { + "@id": "cred:issued", + "@type": "xsd:dateTime" + }, + "issuer": { + "@id": "cred:issuer", + "@type": "@id" + }, + "issuanceDate": { + "@id": "cred:issuanceDate", + "@type": "xsd:dateTime" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": { + "@id": "cred:termsOfUse", + "@type": "@id" + }, + "validFrom": { + "@id": "cred:validFrom", + "@type": "xsd:dateTime" + }, + "validUntil": { + "@id": "cred:validUntil", + "@type": "xsd:dateTime" + } + } + }, + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "verifiableCredential": { + "@id": "cred:verifiableCredential", + "@type": "@id", + "@container": "@graph" + } + } + }, + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/did-v1.jsonld b/inspector-vc/src/main/resources/contexts/did-v1.jsonld new file mode 100644 index 0000000..55ab11c --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/did-v1.jsonld @@ -0,0 +1,58 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/obv3.jsonld b/inspector-vc/src/main/resources/contexts/obv3.jsonld new file mode 100644 index 0000000..301efa5 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/obv3.jsonld @@ -0,0 +1,440 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + + "xsd": "https://www.w3.org/2001/XMLSchema#", + + "OpenBadgeCredential": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#OpenBadgeCredential" + }, + "Achievement": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Achievement", + "@context": { + "achievementType": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#achievementType", + "@type": "xsd:string" + }, + "alignment": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#alignment", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Alignment", + "@container": "@set" + }, + "creator": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile" + }, + "creditsAvailable": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#creditsAvailable", + "@type": "xsd:float" + }, + "criteria": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Criteria", + "@type": "@id" + }, + "fieldOfStudy": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#fieldOfStudy", + "@type": "xsd:string" + }, + "humanCode": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#humanCode", + "@type": "xsd:string" + }, + "otherIdentifier": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#otherIdentifier", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry", + "@container": "@set" + }, + "related": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#related", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Related", + "@container": "@set" + }, + "resultDescription": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultDescription", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#ResultDescription", + "@container": "@set" + }, + "specialization": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#specialization", + "@type": "xsd:string" + }, + "tag": { + "@id": "https://schema.org/keywords", + "@type": "xsd:string", + "@container": "@set" + }, + "version": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#version", + "@type": "xsd:string" + } + } + }, + "AchievementCredential": { + "@id": "OpenBadgeCredential" + }, + "AchievementSubject": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#AchievementSubject", + "@context": { + "achievement": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Achievement" + }, + "activityEndDate": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#activityEndDate", + "@type": "xsd:date" + }, + "activityStartDate": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#activityStartDate", + "@type": "xsd:date" + }, + "creditsEarned": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#creditsEarned", + "@type": "xsd:float" + }, + "identifier": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifier", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentityObject", + "@container": "@set" + }, + "licenseNumber": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#licenseNumber", + "@type": "xsd:string" + }, + "result": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#result", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Result", + "@container": "@set" + }, + "role": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#role", + "@type": "xsd:string" + }, + "source": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#source", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile" + }, + "term": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#term", + "@type": "xsd:string" + } + } + }, + "Address": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Address", + "@context": { + "addressCountry": { + "@id": "https://schema.org/addressCountry", + "@type": "xsd:string" + }, + "addressCountryCode": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#CountryCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "https://schema.org/addresLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "https://schema.org/addressRegion", + "@type": "xsd:string" + }, + "geo": { + "@id" : "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#GeoCoordinates" + }, + "postOfficeBoxNumber": { + "@id": "https://schema.org/postOfficeBoxNumber", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "https://schema.org/postalCode", + "@type": "xsd:string" + }, + "streetAddress": { + "@id": "https://schema.org/streetAddress", + "@type": "xsd:string" + } + } + }, + "Alignment": { + "@id": "https://schema.org/Alignment", + "@context": { + "targetCode": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#targetCode", + "@type": "xsd:string" + }, + "targetDescription": { + "@id": "https://schema.org/targetDescription", + "@type": "xsd:string" + }, + "targetFramework": { + "@id": "https://schema.org/targetFramework", + "@type": "xsd:string" + }, + "targetName": { + "@id": "https://schema.org/targetName", + "@type": "xsd:string" + }, + "targetType": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#targetType", + "@type": "xsd:string" + }, + "targetUrl": { + "@id": "https://schema.org/targetUrl", + "@type": "xsd:anyURI" + } + } + }, + "Criteria": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Criteria" + }, + "EndorsementCredential": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementCredential" + }, + "EndorsementSubject": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementSubject", + "@context": { + "endorsementComment": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#endorsementComment", + "@type": "xsd:string" + } + } + }, + "Evidence": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Evidence", + "@context": { + "audience": { + "@id": "https://schema.org/audience", + "@type": "xsd:string" + }, + "genre": { + "@id": "https://schema.org/genre", + "@type": "xsd:string" + } + } + }, + "GeoCoordinates": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#GeoCoordinates", + "@context": { + "latitude": { + "@id": "https://schema.org/latitude", + "@type": "xsd:string" + }, + "longitude": { + "@id": "https://schema.org/longitude", + "@type": "xsd:string" + } + } + }, + "IdentifierEntry": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry", + "@context": { + "identifier": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifier", + "@type": "xsd:string" + }, + "identifierType": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identifierType", + "@type": "xsd:string" + } + } + }, + "IdentityObject": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentityObject", + "@context": { + "hashed": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#hashed", + "@type": "xsd:boolean" + }, + "identityHash": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identityHash", + "@type": "xsd:string" + }, + "identityType": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#identityType", + "@type": "xsd:string" + }, + "salt": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#salt", + "@type": "xsd:string" + } + } + }, + "Image": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Image", + "@context": { + "caption": { + "@id": "https://schema.org/caption", + "@type": "xsd:string" + } + } + }, + "Profile": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Profile", + "@context": { + "additionalName": { + "@id": "https://schema.org/additionalName", + "@type": "xsd:string" + }, + "address": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Address" + }, + "dateOfBirth": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#dateOfBirth", + "@type": "xsd:date" + }, + "email": { + "@id": "https://schema.org/email", + "@type": "xsd:string" + }, + "familyName": { + "@id": "https://schema.org/familyName", + "@type": "xsd:string" + }, + "familyNamePrefix": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#familyNamePrefix", + "@type": "xsd:string" + }, + "givenName": { + "@id": "https://schema.org/givenName", + "@type": "xsd:string" + }, + "honorificPrefix": { + "@id": "https://schema.org/honorificPrefix", + "@type": "xsd:string" + }, + "honorificSuffix": { + "@id": "https://schema.org/honorificSuffix", + "@type": "xsd:string" + }, + "otherIdentifier": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#otherIdentifier", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#IdentifierEntry", + "@container": "@set" + }, + "parentOrg": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#parentOrg", + "@type": "xsd:string" + }, + "patronymicName": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#patronymicName", + "@type": "xsd:string" + }, + "phone": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#PhoneNumber", + "@type": "xsd:string" + }, + "official": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#official", + "@type": "xsd:string" + } + } + }, + "Related": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Related", + "@context": { + "version": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#version", + "@type": "xsd:string" + } + } + }, + "Result": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Result", + "@context": { + "achievedLevel": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#achievedLevel", + "@type": "xsd:anyURI" + }, + "resultDescription": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultDescription", + "@type": "xsd:anyURI" + }, + "status": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#status", + "@type": "xsd:string" + }, + "value": { + "@id": "https://schema.org/value", + "@type": "xsd:string" + } + } + }, + "ResultDescription": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#ResultDescription", + "@context": { + "allowedValue": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#allowedValue", + "@type": "xsd:string", + "@container": "@set" + }, + "requiredLevel": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#requiredLevel", + "@type": "xsd:anyURI" + }, + "requiredValue": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#requiredValue", + "@type": "xsd:string" + }, + "resultType": { + "@id":"https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#resultType", + "@type": "xsd:string" + }, + "rubricCriterionLevel": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#rubricCriterionLevel", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#RubricCriterionLevel", + "@container": "@set" + }, + "valueMax": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#valueMax", + "@type": "xsd:string" + }, + "valueMin": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#valueMin", + "@type": "xsd:string" + } + } + }, + "RubricCriterionLevel": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#RubricCriterionLevel", + "@context": { + "level": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#level", + "@type": "xsd:string" + }, + "points": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#points", + "@type": "xsd:string" + } + } + }, + "alignment": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#alignment", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Alignment", + "@container": "@set" + }, + "description": { + "@id": "https://schema.org/description", + "@type": "xsd:string" + }, + "endorsement": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#endorsement", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#EndorsementCredential", + "@container": "@set" + }, + "image": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#image", + "@type": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#Image" + }, + "name": { + "@id": "https://schema.org/name", + "@type": "xsd:string" + }, + "narrative": { + "@id": "https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#narrative", + "@type": "xsd:string" + }, + "url": { + "@id": "https://schema.org/url", + "@type": "xsd:anyURI" + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/odrl.jsonld b/inspector-vc/src/main/resources/contexts/odrl.jsonld new file mode 100644 index 0000000..1da5c13 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/odrl.jsonld @@ -0,0 +1,301 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "dct": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "foaf": "http://xmlns.com/foaf/0.1/", + "schema": "http://schema.org/", + "cc": "http://creativecommons.org/ns#", + "uid": "@id", + "type": "@type", + "Policy": "odrl:Policy", + "Rule": "odrl:Rule", + "profile": { + "@type": "@id", + "@id": "odrl:profile" + }, + "inheritFrom": { + "@type": "@id", + "@id": "odrl:inheritFrom" + }, + "ConflictTerm": "odrl:ConflictTerm", + "conflict": { + "@type": "@vocab", + "@id": "odrl:conflict" + }, + "perm": "odrl:perm", + "prohibit": "odrl:prohibit", + "invalid": "odrl:invalid", + "Agreement": "odrl:Agreement", + "Assertion": "odrl:Assertion", + "Offer": "odrl:Offer", + "Privacy": "odrl:Privacy", + "Request": "odrl:Request", + "Set": "odrl:Set", + "Ticket": "odrl:Ticket", + "Asset": "odrl:Asset", + "AssetCollection": "odrl:AssetCollection", + "relation": { + "@type": "@id", + "@id": "odrl:relation" + }, + "hasPolicy": { + "@type": "@id", + "@id": "odrl:hasPolicy" + }, + "target": { + "@type": "@id", + "@id": "odrl:target" + }, + "output": { + "@type": "@id", + "@id": "odrl:output" + }, + "partOf": { + "@type": "@id", + "@id": "odrl:partOf" + }, + "source": { + "@type": "@id", + "@id": "odrl:source" + }, + "Party": "odrl:Party", + "PartyCollection": "odrl:PartyCollection", + "function": { + "@type": "@vocab", + "@id": "odrl:function" + }, + "PartyScope": "odrl:PartyScope", + "assignee": { + "@type": "@id", + "@id": "odrl:assignee" + }, + "assigner": { + "@type": "@id", + "@id": "odrl:assigner" + }, + "assigneeOf": { + "@type": "@id", + "@id": "odrl:assigneeOf" + }, + "assignerOf": { + "@type": "@id", + "@id": "odrl:assignerOf" + }, + "attributedParty": { + "@type": "@id", + "@id": "odrl:attributedParty" + }, + "attributingParty": { + "@type": "@id", + "@id": "odrl:attributingParty" + }, + "compensatedParty": { + "@type": "@id", + "@id": "odrl:compensatedParty" + }, + "compensatingParty": { + "@type": "@id", + "@id": "odrl:compensatingParty" + }, + "consentingParty": { + "@type": "@id", + "@id": "odrl:consentingParty" + }, + "consentedParty": { + "@type": "@id", + "@id": "odrl:consentedParty" + }, + "informedParty": { + "@type": "@id", + "@id": "odrl:informedParty" + }, + "informingParty": { + "@type": "@id", + "@id": "odrl:informingParty" + }, + "trackingParty": { + "@type": "@id", + "@id": "odrl:trackingParty" + }, + "trackedParty": { + "@type": "@id", + "@id": "odrl:trackedParty" + }, + "contractingParty": { + "@type": "@id", + "@id": "odrl:contractingParty" + }, + "contractedParty": { + "@type": "@id", + "@id": "odrl:contractedParty" + }, + "Action": "odrl:Action", + "action": { + "@type": "@vocab", + "@id": "odrl:action" + }, + "includedIn": { + "@type": "@id", + "@id": "odrl:includedIn" + }, + "implies": { + "@type": "@id", + "@id": "odrl:implies" + }, + "Permission": "odrl:Permission", + "permission": { + "@type": "@id", + "@id": "odrl:permission" + }, + "Prohibition": "odrl:Prohibition", + "prohibition": { + "@type": "@id", + "@id": "odrl:prohibition" + }, + "obligation": { + "@type": "@id", + "@id": "odrl:obligation" + }, + "use": "odrl:use", + "grantUse": "odrl:grantUse", + "aggregate": "odrl:aggregate", + "annotate": "odrl:annotate", + "anonymize": "odrl:anonymize", + "archive": "odrl:archive", + "concurrentUse": "odrl:concurrentUse", + "derive": "odrl:derive", + "digitize": "odrl:digitize", + "display": "odrl:display", + "distribute": "odrl:distribute", + "execute": "odrl:execute", + "extract": "odrl:extract", + "give": "odrl:give", + "index": "odrl:index", + "install": "odrl:install", + "modify": "odrl:modify", + "move": "odrl:move", + "play": "odrl:play", + "present": "odrl:present", + "print": "odrl:print", + "read": "odrl:read", + "reproduce": "odrl:reproduce", + "sell": "odrl:sell", + "stream": "odrl:stream", + "textToSpeech": "odrl:textToSpeech", + "transfer": "odrl:transfer", + "transform": "odrl:transform", + "translate": "odrl:translate", + "Duty": "odrl:Duty", + "duty": { + "@type": "@id", + "@id": "odrl:duty" + }, + "consequence": { + "@type": "@id", + "@id": "odrl:consequence" + }, + "remedy": { + "@type": "@id", + "@id": "odrl:remedy" + }, + "acceptTracking": "odrl:acceptTracking", + "attribute": "odrl:attribute", + "compensate": "odrl:compensate", + "delete": "odrl:delete", + "ensureExclusivity": "odrl:ensureExclusivity", + "include": "odrl:include", + "inform": "odrl:inform", + "nextPolicy": "odrl:nextPolicy", + "obtainConsent": "odrl:obtainConsent", + "reviewPolicy": "odrl:reviewPolicy", + "uninstall": "odrl:uninstall", + "watermark": "odrl:watermark", + "Constraint": "odrl:Constraint", + "LogicalConstraint": "odrl:LogicalConstraint", + "constraint": { + "@type": "@id", + "@id": "odrl:constraint" + }, + "refinement": { + "@type": "@id", + "@id": "odrl:refinement" + }, + "Operator": "odrl:Operator", + "operator": { + "@type": "@vocab", + "@id": "odrl:operator" + }, + "RightOperand": "odrl:RightOperand", + "rightOperand": "odrl:rightOperand", + "rightOperandReference": { + "@type": "xsd:anyURI", + "@id": "odrl:rightOperandReference" + }, + "LeftOperand": "odrl:LeftOperand", + "leftOperand": { + "@type": "@vocab", + "@id": "odrl:leftOperand" + }, + "unit": "odrl:unit", + "dataType": { + "@type": "xsd:anyType", + "@id": "odrl:datatype" + }, + "status": "odrl:status", + "absolutePosition": "odrl:absolutePosition", + "absoluteSpatialPosition": "odrl:absoluteSpatialPosition", + "absoluteTemporalPosition": "odrl:absoluteTemporalPosition", + "absoluteSize": "odrl:absoluteSize", + "count": "odrl:count", + "dateTime": "odrl:dateTime", + "delayPeriod": "odrl:delayPeriod", + "deliveryChannel": "odrl:deliveryChannel", + "elapsedTime": "odrl:elapsedTime", + "event": "odrl:event", + "fileFormat": "odrl:fileFormat", + "industry": "odrl:industry:", + "language": "odrl:language", + "media": "odrl:media", + "meteredTime": "odrl:meteredTime", + "payAmount": "odrl:payAmount", + "percentage": "odrl:percentage", + "product": "odrl:product", + "purpose": "odrl:purpose", + "recipient": "odrl:recipient", + "relativePosition": "odrl:relativePosition", + "relativeSpatialPosition": "odrl:relativeSpatialPosition", + "relativeTemporalPosition": "odrl:relativeTemporalPosition", + "relativeSize": "odrl:relativeSize", + "resolution": "odrl:resolution", + "spatial": "odrl:spatial", + "spatialCoordinates": "odrl:spatialCoordinates", + "systemDevice": "odrl:systemDevice", + "timeInterval": "odrl:timeInterval", + "unitOfCount": "odrl:unitOfCount", + "version": "odrl:version", + "virtualLocation": "odrl:virtualLocation", + "eq": "odrl:eq", + "gt": "odrl:gt", + "gteq": "odrl:gteq", + "lt": "odrl:lt", + "lteq": "odrl:lteq", + "neq": "odrl:neg", + "isA": "odrl:isA", + "hasPart": "odrl:hasPart", + "isPartOf": "odrl:isPartOf", + "isAllOf": "odrl:isAllOf", + "isAnyOf": "odrl:isAnyOf", + "isNoneOf": "odrl:isNoneOf", + "or": "odrl:or", + "xone": "odrl:xone", + "and": "odrl:and", + "andSequence": "odrl:andSequence", + "policyUsage": "odrl:policyUsage" + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld new file mode 100644 index 0000000..8500805 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/security-suites-ed25519-2020-v1.jsonld @@ -0,0 +1,93 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "Ed25519VerificationKey2020": { + "@id": "https://w3id.org/security#Ed25519VerificationKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "publicKeyMultibase": { + "@id": "https://w3id.org/security#publicKeyMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + }, + "Ed25519Signature2020": { + "@id": "https://w3id.org/security#Ed25519Signature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java new file mode 100644 index 0000000..e51222a --- /dev/null +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java @@ -0,0 +1,24 @@ +package org.oneedtech.inspect.vc.util; + +import java.net.URI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.apicatalog.jsonld.document.Document; +import com.apicatalog.jsonld.loader.DocumentLoader; +import com.apicatalog.jsonld.loader.DocumentLoaderOptions; +import com.google.common.io.Resources; + +public class CachingDocumentLoaderTests { + + @Test + void testStaticCachedDocument() { + Assertions.assertDoesNotThrow(()->{ + DocumentLoader loader = new CachingDocumentLoader(); + URI uri = Resources.getResource("contexts/did-v1.jsonld").toURI(); + Document doc = loader.loadDocument(uri, new DocumentLoaderOptions()); + Assertions.assertNotNull(doc); + }); + } +} From 1d051a41b18cbe15877cefbe21ae3097dced07ce Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Tue, 5 Jul 2022 23:50:26 +0200 Subject: [PATCH 14/58] Update CachingDocumentLoaderTests.java --- .../vc/util/CachingDocumentLoaderTests.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java index e51222a..4710258 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java @@ -13,7 +13,7 @@ import com.google.common.io.Resources; public class CachingDocumentLoaderTests { @Test - void testStaticCachedDocument() { + void testStaticCachedDocumentURI() { Assertions.assertDoesNotThrow(()->{ DocumentLoader loader = new CachingDocumentLoader(); URI uri = Resources.getResource("contexts/did-v1.jsonld").toURI(); @@ -21,4 +21,16 @@ public class CachingDocumentLoaderTests { Assertions.assertNotNull(doc); }); } + + @Test + void testStaticCachedDocumentKey() { + Assertions.assertDoesNotThrow(()->{ + DocumentLoader loader = new CachingDocumentLoader(); + URI uri = new URI("https://www.w3.org/ns/did/v1"); + Document doc = loader.loadDocument(uri, new DocumentLoaderOptions()); + Assertions.assertNotNull(doc); + }); + } + + } From 599155ca6e4254f352c6b0d0ac03a6ad4891d36c Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Wed, 6 Jul 2022 09:26:26 -0400 Subject: [PATCH 15/58] Committing to merge in changes. WIP, may delete a lot of this. --- inspector-vc/pom.xml | 9 +- .../inspect/vc/probe/ProofVerifierProbe.java | 232 +++++++++++++++++- .../resources/ob30/simple-old-delete.json | 36 --- 3 files changed, 238 insertions(+), 39 deletions(-) delete mode 100644 inspector-vc/src/test/resources/ob30/simple-old-delete.json diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 3e4ca11..b8684e5 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -35,7 +35,14 @@ org.springframework spring-core 5.0.12.RELEASE - + + + com.google + bitcoinj + 0.11.3 + + + com.apicatalog 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 index e3073aa..4863185 100644 --- 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 @@ -4,14 +4,28 @@ import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPP import java.io.ByteArrayOutputStream; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.PublicKey; import java.security.Security; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Base64.Encoder; +import java.util.Map.Entry; import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.crypto.Signer; +import org.bouncycastle.crypto.signers.Ed25519Signer; +import org.bouncycastle.crypto.signers.RSADigestSigner; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Hex; import org.oneedtech.inspect.core.probe.Probe; @@ -28,6 +42,8 @@ import com.apicatalog.rdf.RdfDataset; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.bitcoin.core.Base58; +import com.google.common.io.BaseEncoding; import io.setl.rdf.normalization.RdfNormalize; @@ -45,9 +61,42 @@ public class ProofVerifierProbe extends Probe { public ReportItems run(Credential crd, RunContext ctx) throws Exception { try { - String canonical = canonicalize(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); + //String document = fetchConanicalDocument(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); + String document = ""; + String proof = fetchConanicalProof(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); //System.out.println(canonical); + + + + /* + Encoder encoder1 = Base64.getEncoder(); + String testSignature = "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"; + String signature = encoder1.encodeToString(testSignature.getBytes()); + + Encoder encoder2 = Base64.getEncoder(); + String testKey = "z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"; + String key64 = encoder2.encodeToString(testKey.getBytes()); + String keyHex = Hex.toHexString(testKey.getBytes()); + + boolean test = validate( + keyHex, + signature, + "", + canonical + ); + */ + + + byte[] docHash = getBytes(document); + byte[] proofHash = getBytes(proof); + // concatenate hash of c14n proof options and hash of c14n document + byte[] combined = mergeArrays(proofHash, docHash); + + boolean test = testSigner(combined); + + + boolean stophere = true; //TODO if proofs fail, report OutCome.Fatal //return fatal("msg", ctx); @@ -57,7 +106,7 @@ public class ProofVerifierProbe extends Probe { return success(ctx); } - private String canonicalize(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { + private String fetchConanicalDocument(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { //clone the incoming credential object so we can modify it freely ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); @@ -81,6 +130,55 @@ public class ProofVerifierProbe extends Probe { return result; } + private String fetchConanicalProof(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { + + //clone the incoming credential object so we can modify it freely + ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); + JsonNode copy = mapper.readTree(crd.asJson().toString()); + + //Get the context node to stitch back in. + JsonNode context = copy.get("@context"); + + //Pull out and use proof node only + JsonNode proof = copy.get("proof"); + + //TODO: Make this better at discarding all, but the linked data proof method. + if(proof.isArray()){ + proof = proof.get(0); + } + + //remove these if they exist + ((ObjectNode)proof).remove("jwt"); + ((ObjectNode)proof).remove("signatureValue"); + ((ObjectNode)proof).remove("proofValue"); + + JsonNode newNode = mapper.createObjectNode(); + ((ObjectNode) newNode).set("@context", context); + //Try to structure this 'to the letter' per a slack with Markus + //((ObjectNode) newNode).set("proof", proof); + + //So that we don't remove nodes while iterating over it save all nodes + Iterator> iter = proof.fields(); + while (iter.hasNext()) { + Entry next = iter.next(); + ((ObjectNode) newNode).set(next.getKey(), next.getValue()); + } + + //create JSON-P Json-LD instance + JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(newNode.toString())); + + //create rdf and normalize + RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); + RdfDataset normalized = RdfNormalize.normalize(dataSet); + + //serialize to string + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Rdf.createWriter(mediaType, os).write(normalized); + String result = StringUtils.stripTrailing(os.toString()); + + return result; + } + private boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { //TODO: continue this implementation. //Pulled in bouncy castle library and made sure this sample compiled only. @@ -93,10 +191,140 @@ public class ProofVerifierProbe extends Probe { final var publicKey = kf.generatePublic(pkSpec); final var signedData = Signature.getInstance("ed25519", provider); signedData.initVerify(publicKey); + //Temporarily remove timestamp signedData.update(timestamp.getBytes()); signedData.update(message.getBytes()); return signedData.verify(Hex.decode(signature)); } + + private boolean testSigner(byte[] concatBytes) throws Exception { + + + final var provider = new BouncyCastleProvider(); + Security.addProvider(provider); + + //var publicKeyBytes = Base64.getUrlDecoder().decode("z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); + //var publicKeyBytes = Base64.getUrlDecoder().decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); + + + //var publicKeyBytes = Base58.decode("z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); + //Key with the first chracter stripped + //var publicKeyBytes = Base58.decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); + + + //A working sample key + //var publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); + + + + + //Base 58 decode minus the z + var publicKeyBytes = Base58.decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); + //The slice out the first two array elements (???) + byte[] slicedArray = Arrays.copyOfRange(publicKeyBytes, 2, 34); + + + + final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), slicedArray); + final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); + final var kf = KeyFactory.getInstance("ed25519", provider); + final var publicKey = kf.generatePublic(pkSpec); + final var signedData = Signature.getInstance("ed25519", provider); + signedData.initVerify(publicKey); + signedData.update(concatBytes); + + boolean whatever = true; + + + //Final step, add signature. + + //Need to do this in java + //const signatureBytes = base58btc.decode(proofValue.substr(1)); + + + var signatureBytes = Base58.decode("3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"); + + return signedData.verify(signatureBytes); + + + + +/* + String hexEncodedPubKey = "z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"; + + // Convert to JCA format + byte[] pubKeyBytes = BaseEncoding.base16().lowerCase().decode(hexEncodedPubKey); + SubjectPublicKeyInfo pubKeyInfo = new SubjectPublicKeyInfo( + new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), pubKeyBytes); + + + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKeyInfo.getEncoded()); + KeyFactory keyFactory = KeyFactory.getInstance("Ed25519", provider); + PublicKey pk = keyFactory.generatePublic(keySpec); + + +*/ + + + + + /* + var test = new RSADigestSigner(digest, digestOid) + + test.verifySignature(signature); + */ + + + + + + + } + + /* + private boolean testSigner3(String message, byte[] concateBytes) throws Exception { + + + // Test case defined in https://www.rfc-editor.org/rfc/rfc8037 + var msg = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc".getBytes(StandardCharsets.UTF_8); + var expectedSig = "hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"; + + var privateKeyBytes = Base64.getUrlDecoder().decode("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); + var publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); + + var privateKey = new Ed25519PrivateKeyParameters(privateKeyBytes, 0); + var publicKey = new Ed25519PublicKeyParameters(publicKeyBytes, 0); + + // Generate new signature + Signer signer = new Ed25519Signer(); + signer.init(true, privateKey); + signer.update(msg, 0, msg.length); + byte[] signature = signer.generateSignature(); + var actualSignature = Base64.getUrlEncoder().encodeToString(signature).replace("=", ""); + + LOG.info("Expected signature: {}", expectedSig); + LOG.info("Actual signature : {}", actualSignature); + + assertEquals(expectedSig, actualSignature); + + + return true; + } + */ + + + private byte[] getBytes(String value) throws Exception{ + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return digest.digest( + value.getBytes(StandardCharsets.UTF_8) + ); + } + + private static byte[] mergeArrays(final byte[] array1, byte[] array2) { + byte[] joinedArray = Arrays.copyOf(array1, array1.length + array2.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + return joinedArray; + } private enum C14n { URDNA2015 diff --git a/inspector-vc/src/test/resources/ob30/simple-old-delete.json b/inspector-vc/src/test/resources/ob30/simple-old-delete.json deleted file mode 100644 index 7795327..0000000 --- a/inspector-vc/src/test/resources/ob30/simple-old-delete.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "@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 From c40e0610237985c39818eb9b5fb67da2dd30fb61 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 6 Jul 2022 15:41:24 +0200 Subject: [PATCH 16/58] throw JsonLdError per spec --- .../inspect/vc/util/CachingDocumentLoader.java | 14 +++++++++----- .../vc/util/CachingDocumentLoaderTests.java | 18 ++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 7032fe2..dab8845 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -7,10 +7,10 @@ import java.time.Duration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.oneedtech.inspect.util.code.Closeables; import org.oneedtech.inspect.util.code.Tuple; import com.apicatalog.jsonld.JsonLdError; +import com.apicatalog.jsonld.JsonLdErrorCode; import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.JsonDocument; import com.apicatalog.jsonld.loader.DocumentLoader; @@ -34,11 +34,11 @@ public class CachingDocumentLoader implements DocumentLoader { return documentCache.get(tpl); } catch (Exception e) { logger.error("contextCache not able to load {}", url); - } - return null; + throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage()); + } } - private static final ImmutableMap bundled = ImmutableMap.builder() + static final ImmutableMap bundled = ImmutableMap.builder() .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) .put("https://w3id.org/security/suites/ed25519-2020/v1", Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) @@ -46,7 +46,7 @@ public class CachingDocumentLoader implements DocumentLoader { .put("https://imsglobal.github.io/openbadges-specification/context.json", Resources.getResource("contexts/obv3.jsonld")) .build(); - private static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder() + static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder() .initialCapacity(32) .maximumSize(64) .expireAfterAccess(Duration.ofHours(24)) @@ -60,5 +60,9 @@ public class CachingDocumentLoader implements DocumentLoader { } }); + public static void reset() { + documentCache.invalidateAll(); + } + private static final Logger logger = LogManager.getLogger(); } diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java index 4710258..b56542f 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java @@ -8,17 +8,17 @@ import org.junit.jupiter.api.Test; import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.loader.DocumentLoader; import com.apicatalog.jsonld.loader.DocumentLoaderOptions; -import com.google.common.io.Resources; public class CachingDocumentLoaderTests { @Test - void testStaticCachedDocumentURI() { + void testStaticCachedDocumentBundled() { Assertions.assertDoesNotThrow(()->{ - DocumentLoader loader = new CachingDocumentLoader(); - URI uri = Resources.getResource("contexts/did-v1.jsonld").toURI(); - Document doc = loader.loadDocument(uri, new DocumentLoaderOptions()); - Assertions.assertNotNull(doc); + DocumentLoader loader = new CachingDocumentLoader(); + for(String id : CachingDocumentLoader.bundled.keySet()) { + Document doc = loader.loadDocument(new URI(id), new DocumentLoaderOptions()); + Assertions.assertNotNull(doc); + } }); } @@ -30,7 +30,5 @@ public class CachingDocumentLoaderTests { Document doc = loader.loadDocument(uri, new DocumentLoaderOptions()); Assertions.assertNotNull(doc); }); - } - - -} + } +} \ No newline at end of file From 502229883e0b1870614b82c7b81abc5ad9039266 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Thu, 7 Jul 2022 09:18:38 -0400 Subject: [PATCH 17/58] This is just a snap shot of attempting to get the proof working with bouncy castle ED25519. Ultimately we found a VC library that handles more of this so we will likely remove. --- .../inspect/vc/probe/ProofVerifierProbe.java | 114 ++---------------- 1 file changed, 8 insertions(+), 106 deletions(-) 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 index 111776a..04d5fe6 100644 --- 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 @@ -32,6 +32,7 @@ import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import com.apicatalog.jsonld.JsonLd; import com.apicatalog.jsonld.StringUtils; @@ -61,33 +62,10 @@ public class ProofVerifierProbe extends Probe { public ReportItems run(Credential crd, RunContext ctx) throws Exception { try { - //String document = fetchConanicalDocument(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); - String document = ""; + String document = fetchConanicalDocument(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); String proof = fetchConanicalProof(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); //System.out.println(canonical); - - - /* - Encoder encoder1 = Base64.getEncoder(); - String testSignature = "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"; - String signature = encoder1.encodeToString(testSignature.getBytes()); - - Encoder encoder2 = Base64.getEncoder(); - String testKey = "z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"; - String key64 = encoder2.encodeToString(testKey.getBytes()); - String keyHex = Hex.toHexString(testKey.getBytes()); - - boolean test = validate( - keyHex, - signature, - "", - canonical - ); - */ - - - byte[] docHash = getBytes(document); byte[] proofHash = getBytes(proof); // concatenate hash of c14n proof options and hash of c14n document @@ -95,8 +73,6 @@ public class ProofVerifierProbe extends Probe { boolean test = testSigner(combined); - - boolean stophere = true; //TODO if proofs fail, report OutCome.Fatal //return fatal("msg", ctx); @@ -118,8 +94,8 @@ public class ProofVerifierProbe extends Probe { //create JSON-P Json-LD instance JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(copy.toString())); - //create rdf and normalize //TODO add DocumentLoader to cache contexts - RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); + //create rdf and normalize + RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).loader(new CachingDocumentLoader()).ordered(true).get(); RdfDataset normalized = RdfNormalize.normalize(dataSet); //serialize to string @@ -134,7 +110,7 @@ public class ProofVerifierProbe extends Probe { //clone the incoming credential object so we can modify it freely ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); - JsonNode copy = mapper.readTree(crd.asJson().toString()); + JsonNode copy = mapper.readTree(crd.getJson().toString()); //Get the context node to stitch back in. JsonNode context = copy.get("@context"); @@ -168,7 +144,8 @@ public class ProofVerifierProbe extends Probe { JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(newNode.toString())); //create rdf and normalize - RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); + //RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); + RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).loader(new CachingDocumentLoader()).ordered(true).get(); RdfDataset normalized = RdfNormalize.normalize(dataSet); //serialize to string @@ -179,52 +156,18 @@ public class ProofVerifierProbe extends Probe { return result; } - private boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { - //TODO: continue this implementation. - //Pulled in bouncy castle library and made sure this sample compiled only. - final var provider = new BouncyCastleProvider(); - Security.addProvider(provider); - final var byteKey = Hex.decode(pubkey); - final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), byteKey); - final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); - final var kf = KeyFactory.getInstance("ed25519", provider); - final var publicKey = kf.generatePublic(pkSpec); - final var signedData = Signature.getInstance("ed25519", provider); - signedData.initVerify(publicKey); - //Temporarily remove timestamp - signedData.update(timestamp.getBytes()); - signedData.update(message.getBytes()); - return signedData.verify(Hex.decode(signature)); - } - private boolean testSigner(byte[] concatBytes) throws Exception { final var provider = new BouncyCastleProvider(); Security.addProvider(provider); - //var publicKeyBytes = Base64.getUrlDecoder().decode("z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); - //var publicKeyBytes = Base64.getUrlDecoder().decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); - - - //var publicKeyBytes = Base58.decode("z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); - //Key with the first chracter stripped - //var publicKeyBytes = Base58.decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); - - - //A working sample key - //var publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); - - - - //Base 58 decode minus the z var publicKeyBytes = Base58.decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); //The slice out the first two array elements (???) byte[] slicedArray = Arrays.copyOfRange(publicKeyBytes, 2, 34); - final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), slicedArray); final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); final var kf = KeyFactory.getInstance("ed25519", provider); @@ -233,54 +176,13 @@ public class ProofVerifierProbe extends Probe { signedData.initVerify(publicKey); signedData.update(concatBytes); - boolean whatever = true; - - - //Final step, add signature. - - //Need to do this in java - //const signatureBytes = base58btc.decode(proofValue.substr(1)); - - var signatureBytes = Base58.decode("3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"); return signedData.verify(signatureBytes); - - - - -/* - String hexEncodedPubKey = "z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"; - - // Convert to JCA format - byte[] pubKeyBytes = BaseEncoding.base16().lowerCase().decode(hexEncodedPubKey); - SubjectPublicKeyInfo pubKeyInfo = new SubjectPublicKeyInfo( - new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), pubKeyBytes); - - - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(pubKeyInfo.getEncoded()); - KeyFactory keyFactory = KeyFactory.getInstance("Ed25519", provider); - PublicKey pk = keyFactory.generatePublic(keySpec); - - -*/ - - - - - /* - var test = new RSADigestSigner(digest, digestOid) - - test.verifySignature(signature); - */ - - - - - } + //An alternate path with bouncy castle /* private boolean testSigner3(String message, byte[] concateBytes) throws Exception { From 9d3110547e88d4eff461aeaabbc3aa6fdfe375f4 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 8 Jul 2022 11:33:45 +0200 Subject: [PATCH 18/58] add iron for vc verification, refactor+cleanup --- inspector-vc/pom.xml | 45 ++-- .../org/oneedtech/inspect/vc/Credential.java | 28 +- .../oneedtech/inspect/vc/OB30Inspector.java | 144 +++++----- .../inspect/vc/payload/JsonParser.java | 32 +++ .../inspect/vc/payload/JwtParser.java | 32 +++ .../inspect/vc/payload/PayloadParser.java | 52 ++++ .../vc/payload/PayloadParserFactory.java | 25 ++ .../inspect/vc/payload/PngParser.java | 101 +++++++ .../inspect/vc/payload/SvgParser.java | 79 ++++++ .../vc/probe/CredentialParseProbe.java | 54 ++++ .../inspect/vc/probe/CredentialTypeProbe.java | 234 ----------------- .../vc/probe/InlineJsonSchemaProbe.java | 24 +- .../vc/probe/IssuanceVerifierProbe.java | 2 +- .../inspect/vc/probe/Predicates.java | 51 ---- .../inspect/vc/probe/ProofVerifierProbe.java | 246 +++--------------- .../vc/probe/SignatureVerifierProbe.java | 32 ++- .../inspect/vc/probe/TypePropertyProbe.java | 55 ++++ .../vc/util/CachingDocumentLoader.java | 4 +- .../inspect/vc/util/JsonNodeUtil.java | 24 +- .../org/oneedtech/inspect/vc/OB30Tests.java | 60 ++++- .../org/oneedtech/inspect/vc/Samples.java | 3 + .../vc/credential/PayloadParserTests.java | 119 +++++++++ .../inspect/vc/util/JsonNodeUtilTests.java | 27 +- .../test/resources/ob30/simple-expired.json | 37 +++ .../test/resources/ob30/simple-issued.json | 37 +++ .../resources/ob30/simple-proof-error.json | 36 +++ 26 files changed, 923 insertions(+), 660 deletions(-) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java delete mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java delete mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java create mode 100644 inspector-vc/src/test/resources/ob30/simple-expired.json create mode 100644 inspector-vc/src/test/resources/ob30/simple-issued.json create mode 100644 inspector-vc/src/test/resources/ob30/simple-proof-error.json diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index b8684e5..23022ba 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -1,20 +1,17 @@ - - 4.0.0 - - org.1edtech - inspector - 0.9.2 - - inspector-vc - + + 4.0.0 + + org.1edtech + inspector + 0.9.2 + + inspector-vc + org.1edtech - inspector-core - - - org.bouncycastle - bcprov-jdk15to18 - 1.65 + inspector-core com.auth0 @@ -32,17 +29,10 @@ 3.10.3 - org.springframework - spring-core - 5.0.12.RELEASE + com.apicatalog + iron-verifiable-credentials-jre8 + 0.7.0 - - com.google - bitcoinj - 0.11.3 - - - com.apicatalog @@ -50,7 +40,7 @@ 1.3.1 - + org.glassfish jakarta.json 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 index dce7153..e64ac98 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -4,6 +4,7 @@ import static org.oneedtech.inspect.util.code.Defensives.*; import static org.oneedtech.inspect.util.resource.ResourceType.*; import java.util.Iterator; +import java.util.List; import java.util.Optional; import org.oneedtech.inspect.core.probe.GeneratedObject; @@ -29,18 +30,20 @@ public class Credential extends GeneratedObject { public Credential(Resource resource, JsonNode data, String jwt) { 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; - this.jwt = jwt; - - ArrayNode typeNode = (ArrayNode)jsonData.get("type"); + this.resource = checkNotNull(resource); + this.jsonData = checkNotNull(data); + this.jwt = jwt; //may be null + + checkTrue(RECOGNIZED_PAYLOAD_TYPES.contains(resource.getType())); + + ArrayNode typeNode = (ArrayNode)jsonData.get("type"); this.credentialType = Credential.Type.valueOf(typeNode); } - + + public Credential(Resource resource, JsonNode data) { + this(resource, data, null); + } + public Resource getResource() { return resource; } @@ -53,8 +56,8 @@ public class Credential extends GeneratedObject { return credentialType; } - public String getJwt() { - return jwt; + public Optional getJwt() { + return Optional.ofNullable(jwt); } /** @@ -108,5 +111,6 @@ public class Credential extends GeneratedObject { } public static final String ID = Credential.class.getCanonicalName(); + public static final List RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 16de1ae..a6518fd 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -1,24 +1,24 @@ package org.oneedtech.inspect.vc; -import static com.google.common.base.Strings.isNullOrEmpty; 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.util.JsonNodeUtil.getEndorsements; +import static org.oneedtech.inspect.vc.Credential.Type.OpenBadgeCredential; +import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY; +import static org.oneedtech.inspect.vc.payload.PayloadParser.fromJwt; +import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; -import org.oneedtech.inspect.core.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; @@ -30,14 +30,16 @@ 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.CredentialParseProbe; 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 org.oneedtech.inspect.vc.probe.TypePropertyProbe; +import org.oneedtech.inspect.vc.util.CachingDocumentLoader; +import org.oneedtech.inspect.vc.util.JsonNodeUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -60,9 +62,12 @@ public class OB30Inspector extends VCInspector { @Override public Report run(Resource resource) { - super.check(resource); + super.check(resource); //TODO because URIs, this should be a fetch and cache - if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) JsonSchemaCache.reset(); + if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) { + JsonSchemaCache.reset(); + CachingDocumentLoader.reset(); + } ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); @@ -77,88 +82,79 @@ public class OB30Inspector extends VCInspector { List accumulator = new ArrayList<>(); int probeCount = 0; - try { - //TODO turn into a loop once stable - + try { //detect type (png, svg, json, jwt) and extract json data probeCount++; - accumulator.add(new CredentialTypeProbe().run(resource, ctx)); + accumulator.add(new CredentialParseProbe().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 + + //type property probeCount++; - accumulator.add(new JsonArrayProbe(vcType).run(crd.getJson(), ctx)); - probeCount++; - accumulator.add(new JsonArrayProbe(obType).run(crd.getJson(), 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.getJson(), ctx)); - - //validate against any inline schemas - probeCount++; - accumulator.add(new InlineJsonSchemaProbe().run(crd, ctx)); - - //If this credential was originally contained in a JWT we must validate the jwt and external proof. - if(!isNullOrEmpty(crd.getJwt())){ + accumulator.add(new TypePropertyProbe(OpenBadgeCredential).run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //canonical schema and inline schemata + SchemaKey schema = crd.getSchemaKey().orElseThrow(); + for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { probeCount++; + accumulator.add(probe.run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //signatures, proofs + probeCount++; + if(crd.getJwt().isPresent()){ + //The credential originally contained in a JWT, validate the jwt and external proof. accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } else { + //The credential not contained in a jwt, must have an internal proof. + accumulator.add(new ProofVerifierProbe().run(crd, ctx)); } - - //verify proofs TODO @Miles - //If this credential was not contained in a jwt it must have an internal proof. - if(isNullOrEmpty(crd.getJwt())){ - probeCount++; - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - + 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); - if(newID.isPresent()) { + if(newID.isPresent()) { + //TODO resource.type 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.getJson(), jsonPath); - if(endorsements.size() > 0) { - EndorsementInspector subInspector = new EndorsementInspector.Builder().build(); - for(JsonNode endorsementNode : endorsements) { - probeCount++; - //TODO: @Markus @Miles, need to refactor to detect as this can be an internal or external proof credential. - //This will LIKELY come from two distinct sources in which case we would detect the type by property name. - //Third param to constructor: Compact JWT -> add third param after decoding. Internal Proof, null jwt string. - //Credential endorsement = new Credential(resource, endorsementNode); - //accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); - } + + //revocation, expiration and issuance + for(Probe probe : List.of(new RevocationListProbe(), + new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { + probeCount++; + accumulator.add(probe.run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } + + //embedded endorsements + EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); + List endorsements = JsonNodeUtil.asNodeList(crd.getJson(), "$..endorsement", jsonPath); + for(JsonNode node : endorsements) { + probeCount++; + Credential endorsement = new Credential(resource, node); + accumulator.add(endorsementInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); + } + + //embedded jwt endorsements + endorsements = JsonNodeUtil.asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); + for(JsonNode node : endorsements) { + probeCount++; + String jwt = node.asText(); + JsonNode vcNode = fromJwt(jwt, ctx); + Credential endorsement = new Credential(resource, vcNode, jwt); + accumulator.add(endorsementInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); + } + //finally, run any user-added probes for(Probe probe : userProbes) { probeCount++; @@ -193,13 +189,7 @@ public class OB30Inspector extends VCInspector { } 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 diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java new file mode 100644 index 0000000..6552312 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java @@ -0,0 +1,32 @@ +package org.oneedtech.inspect.vc.payload; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.oneedtech.inspect.util.code.Defensives.checkTrue; + +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.vc.Credential; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A credential extractor for JSON files. + * @author mgylling + */ +public final class JsonParser extends PayloadParser { + + @Override + public boolean supports(ResourceType type) { + return type == ResourceType.JSON; + } + + @Override + public Credential parse(Resource resource, RunContext ctx) throws Exception { + checkTrue(resource.getType() == ResourceType.JSON); + String json = resource.asByteSource().asCharSource(UTF_8).read(); + JsonNode node = fromString(json, ctx); + return new Credential(resource, node); + } + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java new file mode 100644 index 0000000..3a946b1 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java @@ -0,0 +1,32 @@ +package org.oneedtech.inspect.vc.payload; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.oneedtech.inspect.util.code.Defensives.checkTrue; + +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.vc.Credential; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A credential extractor for JWT files. + * @author mgylling + */ +public final class JwtParser extends PayloadParser { + + @Override + public boolean supports(ResourceType type) { + return type == ResourceType.JWT; + } + + @Override + public Credential parse(Resource resource, RunContext ctx) throws Exception { + checkTrue(resource.getType() == ResourceType.JWT); + String jwt = resource.asByteSource().asCharSource(UTF_8).read(); + JsonNode node = fromJwt(jwt, ctx); + return new Credential(resource, node, jwt); + } + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java new file mode 100644 index 0000000..41ccb2c --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java @@ -0,0 +1,52 @@ +package org.oneedtech.inspect.vc.payload; + +import java.util.Base64; +import java.util.List; +import java.util.Base64.Decoder; + +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.vc.Credential; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Splitter; + +/** + * Abstract base for extracting Credential instances from payloads. + * @author mgylling + */ +public abstract class PayloadParser { + + public abstract boolean supports(ResourceType type); + + public abstract Credential parse(Resource source, RunContext ctx) throws Exception; + + protected static JsonNode fromString(String json, RunContext context) throws Exception { + return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json); + } + + /** + * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof + * @return The decoded JSON String + */ + public static JsonNode fromJwt(String jwt, RunContext context) throws Exception { + List parts = Splitter.on('.').splitToList(jwt); + if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); + + final Decoder decoder = Base64.getUrlDecoder(); + /* + * For this step we are only deserializing the stored badge out of the payload. + * The entire jwt is stored separately for signature verification later. + */ + String jwtPayload = new String(decoder.decode(parts.get(1))); + + //Deserialize and fetch the 'vc' node from the object + JsonNode outerPayload = fromString(jwtPayload, context); + JsonNode vcNode = outerPayload.get("vc"); + + return vcNode; + } + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java new file mode 100644 index 0000000..a2f0cc3 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParserFactory.java @@ -0,0 +1,25 @@ +package org.oneedtech.inspect.vc.payload; + +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; + +import java.util.List; + +import org.oneedtech.inspect.util.resource.Resource; + +/** + * A factory to create PayloadParser instances for various resource types. + * @author mgylling + */ +public class PayloadParserFactory { + private static final Iterable parsers = List.of( + new PngParser(), new SvgParser(), + new JsonParser(), new JwtParser()); + + public static PayloadParser of(Resource resource) { + checkNotNull(resource.getType()); + for(PayloadParser cex : parsers) { + if(cex.supports(resource.getType())) return cex; + } + throw new IllegalArgumentException(); + } +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java new file mode 100644 index 0000000..9d0f8d8 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java @@ -0,0 +1,101 @@ +package org.oneedtech.inspect.vc.payload; + +import static org.oneedtech.inspect.util.code.Defensives.checkTrue; + +import java.io.InputStream; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; + +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.vc.Credential; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A credential extractor for PNG images. + * @author mgylling + */ +public final class PngParser extends PayloadParser { + + @Override + public boolean supports(ResourceType type) { + return type == ResourceType.PNG; + } + + @Override + public Credential parse(Resource resource, RunContext ctx) throws Exception { + + checkTrue(resource.getType() == ResourceType.PNG); + + try(InputStream is = resource.asByteSource().openStream()) { + + ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); + imageReader.setInput(ImageIO.createImageInputStream(is), true); + IIOMetadata metadata = imageReader.getImageMetadata(0); + + String credentialString = null; + String jwtString = null; + String formatSearch = null; + JsonNode credential = null; + + String[] names = metadata.getMetadataFormatNames(); + int length = names.length; + for (int i = 0; i < length; i++) { + //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0") + formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i])); + if(formatSearch != null) { + credentialString = formatSearch; + } + } + + if(credentialString == null) { + throw new IllegalArgumentException("No credential inside PNG"); + } + + credentialString = credentialString.trim(); + if(credentialString.charAt(0) != '{'){ + //This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification. + jwtString = credentialString; + credential = fromJwt(credentialString, ctx); + } + else { + credential = fromString(credentialString, ctx); + } + + return new Credential(resource, credential, jwtString); + } + } + + private String getOpenBadgeCredentialNodeText(Node node){ + NamedNodeMap attributes = node.getAttributes(); + + //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one. + Node keyword = attributes.getNamedItem("keyword"); + if(keyword != null && keyword.getNodeValue().equals("openbadgecredential")){ + Node textAttribute = attributes.getNamedItem("text"); + if(textAttribute != null) { + return textAttribute.getNodeValue(); + } + } + + //iterate over all children depth first and search for the credential node. + Node child = node.getFirstChild(); + while (child != null) { + String nodeValue = getOpenBadgeCredentialNodeText(child); + if(nodeValue != null) { + return nodeValue; + } + child = child.getNextSibling(); + } + + //Return null if we haven't found anything at this recursive depth. + return null; + } + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java new file mode 100644 index 0000000..f600bc9 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java @@ -0,0 +1,79 @@ +package org.oneedtech.inspect.vc.payload; + +import static org.oneedtech.inspect.util.code.Defensives.checkTrue; + +import java.io.InputStream; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Characters; +import javax.xml.stream.events.XMLEvent; + +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.util.code.Defensives; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.util.xml.XMLInputFactoryCache; +import org.oneedtech.inspect.vc.Credential; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * A credential extractor for SVG documents. + * @author mgylling + */ +public final class SvgParser extends PayloadParser { + + @Override + public boolean supports(ResourceType type) { + return type == ResourceType.SVG; + } + + @Override + public Credential parse(Resource resource, RunContext ctx) throws Exception { + + checkTrue(resource.getType() == ResourceType.SVG); + + try(InputStream is = resource.asByteSource().openStream()) { + XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is); + while(reader.hasNext()) { + XMLEvent ev = reader.nextEvent(); + if(isEndElem(ev, OB_CRED_ELEM)) break; + if(isStartElem(ev, OB_CRED_ELEM)) { + Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR); + if(verifyAttr != null) { + String jwt = verifyAttr.getValue(); + JsonNode node = fromJwt(jwt, ctx); + return new Credential(resource, node, jwt); + } else { + while(reader.hasNext()) { + ev = reader.nextEvent(); + if(isEndElem(ev, OB_CRED_ELEM)) break; + if(ev.getEventType() == XMLEvent.CHARACTERS) { + Characters chars = ev.asCharacters(); + if(!chars.isWhiteSpace()) { + JsonNode node = fromString(chars.getData(), ctx); + return new Credential(resource, node); + } + } + } + } + } + } //while(reader.hasNext()) { + } + throw new IllegalArgumentException("No credential inside SVG"); + + } + + private boolean isEndElem(XMLEvent ev, QName name) { + return ev.isEndElement() && ev.asEndElement().getName().equals(name); + } + + private boolean isStartElem(XMLEvent ev, QName name) { + return ev.isStartElement() && ev.asStartElement().getName().equals(name); + } + + private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential"); + private static final QName OB_CRED_VERIFY_ATTR = new QName("verify"); +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java new file mode 100644 index 0000000..02a549b --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java @@ -0,0 +1,54 @@ +package org.oneedtech.inspect.vc.probe; + +import java.util.Optional; + +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.util.resource.detect.TypeDetector; +import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.payload.PayloadParserFactory; + +/** + * A probe that verifies that the incoming credential resource is of a recognized payload type + * and if so extracts and stores the VC json data (a 'Credential' instance) + * in the RunContext. + * @author mgylling + */ +public class CredentialParseProbe extends Probe { + + @Override + public ReportItems run(Resource resource, RunContext context) throws Exception { + + try { + + //TODO if .detect reads from a URIResource twice. Cache the resource on first call. + + Optional type = Optional.ofNullable(resource.getType()); + if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) { + type = TypeDetector.detect(resource, true); + if(type.isEmpty()) { + //TODO if URI fetch, TypeDetector likely to fail + System.err.println("typedetector fail: extend behavior here"); + return fatal("Could not detect credential payload type", context); + } else { + resource.setType(type.get()); + } + } + + if(!Credential.RECOGNIZED_PAYLOAD_TYPES.contains(type.get())) { + return fatal("Payload type not supported: " + type.get().getName(), context); + } + + Credential crd = PayloadParserFactory.of(resource).parse(resource, context); + context.addGeneratedObject(crd); + return success(this, context); + + } catch (Exception e) { + return fatal("Error while parsing credential: " + e.getMessage(), context); + } + } + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java deleted file mode 100644 index 1634a82..0000000 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.oneedtech.inspect.vc.probe; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.InputStream; -import java.util.Base64; -import java.util.Base64.Decoder; -import java.util.List; -import java.util.Optional; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.metadata.IIOMetadata; -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 org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; - -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 { - - //TODO: this reads from a URIResource twice. Cache the resource on first call. - - Optional type = TypeDetector.detect(resource, true); - - if(type.isPresent()) { - resource.setType(type.get()); - if(type.get() == ResourceType.PNG) { - crd = fromPNG(resource, context); - } else if(type.get() == ResourceType.SVG) { - crd = fromSVG(resource, context); - } else if(type.get() == ResourceType.JSON) { - crd = fromJson(resource, context); - } else if(type.get() == ResourceType.JWT) { - crd = 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 Credential fromPNG(Resource resource, RunContext context) throws Exception { - try(InputStream is = resource.asByteSource().openStream()) { - ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); - imageReader.setInput(ImageIO.createImageInputStream(is), true); - IIOMetadata metadata = imageReader.getImageMetadata(0); - - String credentialString = null; - String jwtString = null; - String formatSearch = null; - JsonNode credential = null; - String[] names = metadata.getMetadataFormatNames(); - int length = names.length; - for (int i = 0; i < length; i++) - { - //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0") - formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i])); - if(formatSearch != null) { credentialString = formatSearch; } - } - - if(credentialString == null) { throw new IllegalArgumentException("No credential inside PNG"); } - - credentialString = credentialString.trim(); - if(credentialString.charAt(0) != '{'){ - //This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification. - jwtString = credentialString; - credential = decodeJWT(credentialString,context); - } - else { - credential = buildNodeFromString(credentialString, context); - } - - return new Credential(resource, credential, jwtString); - } - } - - /** - * Extract the JSON data from a baked SVG credential. - * @param context - * @throws Exception - */ - private Credential fromSVG(Resource resource, RunContext context) throws Exception { - String json = null; - String jwtString = null; - JsonNode credential = 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) { - jwtString = verifyAttr.getValue(); - credential = decodeJWT(verifyAttr.getValue(), context); - 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(); - credential = buildNodeFromString(json, context); - break; - } - } - } - } - } - if(credential!=null) break; - } - } - if(credential == null) throw new IllegalArgumentException("No credential inside SVG"); - return new Credential(resource, credential, jwtString); - } - - /** - * Create a Credential object from a raw JSON resource. - * @param context - */ - private Credential fromJson(Resource resource, RunContext context) throws Exception { - return new Credential(resource, buildNodeFromString(resource.asByteSource().asCharSource(UTF_8).read(), context), null); - } - - /** - * Create a Credential object from a JWT resource. - * @param context - */ - private Credential fromJWT(Resource resource, RunContext context) throws Exception { - return new Credential( - resource, - decodeJWT( - resource.asByteSource().asCharSource(UTF_8).read(), - context - ) - , resource.asByteSource().asCharSource(UTF_8).read() - ); - } - - /** - * Create a JsonNode object from a String. - */ - private JsonNode buildNodeFromString(String json, RunContext context) throws Exception { - return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json); - } - - /** - * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof - * @return The decoded JSON String - */ - private JsonNode decodeJWT(String jwt, RunContext context) throws Exception { - List parts = Splitter.on('.').splitToList(jwt); - if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); - - final Decoder decoder = Base64.getUrlDecoder(); - //For this step we are only deserializing the stored badge out of the payload. The entire jwt is stored separately for - //signature verification later. - String jwtPayload = new String(decoder.decode(parts.get(1))); - - //Deserialize and fetch the 'vc' node from the object - JsonNode outerPayload = buildNodeFromString(jwtPayload, context); - JsonNode vcNode = outerPayload.get("vc"); - - return vcNode; - } - - private String getOpenBadgeCredentialNodeText(Node node){ - NamedNodeMap attributes = node.getAttributes(); - - //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one. - if(attributes.getNamedItem("keyword") != null && attributes.getNamedItem("keyword").getNodeValue().equals("openbadgecredential")){ - Node textAttribute = attributes.getNamedItem("text"); - if(textAttribute != null) { return textAttribute.getNodeValue(); } - } - - //iterate over all children depth first and search for the credential node. - Node child = node.getFirstChild(); - while (child != null) - { - String nodeValue = getOpenBadgeCredentialNodeText(child); - if(nodeValue != null) { return nodeValue; } - child = child.getNextSibling(); - } - - //Return null if we haven't found anything at this recursive depth. - return null; - } - - 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/InlineJsonSchemaProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java index 94f6363..e1179b8 100644 --- 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 @@ -20,16 +20,21 @@ import com.fasterxml.jackson.databind.node.ArrayNode; * Detect inline schemas in a credential and run them. * @author mgylling */ -public class InlineJsonSchemaProbe extends Probe { +public class InlineJsonSchemaProbe extends Probe { private static final Set types = Set.of("1EdTechJsonSchemaValidator2019"); - private final boolean skipCanonical = true; - + private SchemaKey skip; + public InlineJsonSchemaProbe() { super(ID); } + public InlineJsonSchemaProbe(SchemaKey skip) { + super(ID); + this.skip = skip; + } + @Override - public ReportItems run(Credential crd, RunContext ctx) throws Exception { + public ReportItems run(JsonNode root, RunContext ctx) throws Exception { List accumulator = new ArrayList<>(); Set ioErrors = new HashSet<>(); @@ -37,7 +42,7 @@ public class InlineJsonSchemaProbe extends Probe { // ArrayNode nodes = jsonPath.eval("$..*[?(@.credentialSchema)]", crd.getJson()); // note - we dont get deep nested ones in e.g. EndorsementCredential - JsonNode credentialSchemaNode = crd.getJson().get("credentialSchema"); + JsonNode credentialSchemaNode = root.get("credentialSchema"); if(credentialSchemaNode == null) return success(ctx); ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast @@ -49,9 +54,9 @@ public class InlineJsonSchemaProbe extends Probe { if(idNode == null) continue; String id = idNode.asText().strip(); if(ioErrors.contains(id)) continue; - if(skipCanonical && equals(crd.getSchemaKey(), id)) continue; + if(equals(skip, id)) continue; try { - accumulator.add(new JsonSchemaProbe(id).run(crd.getJson(), ctx)); + accumulator.add(new JsonSchemaProbe(id).run(root, ctx)); } catch (Exception e) { if(!ioErrors.contains(id)) { ioErrors.add(id); @@ -63,8 +68,9 @@ public class InlineJsonSchemaProbe extends Probe { return new ReportItems(accumulator); } - private boolean equals(Optional key, String id) { - return key.isPresent() && key.get().getCanonicalURI().equals(id); + private boolean equals(SchemaKey key, String id) { + if(key == null) return false; + return key.getCanonicalURI().equals(id); } public static final String ID = InlineJsonSchemaProbe.class.getSimpleName(); diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java index 7eef25a..70e5ee9 100644 --- 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 @@ -34,7 +34,7 @@ public class IssuanceVerifierProbe extends Probe { try { issuanceDate = ZonedDateTime.parse(node.textValue()); if (issuanceDate.isAfter(now)) { - return fatal("The credential is not yet valid (issuance date is " + node.asText() + ").", ctx); + return fatal("The credential is not yet issued (issuance date is " + node.asText() + ").", ctx); } } catch (Exception e) { return exception("Error while checking issuanceDate: " + e.getMessage(), ctx.getResource()); 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 deleted file mode 100644 index e954e36..0000000 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/Predicates.java +++ /dev/null @@ -1,51 +0,0 @@ -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 index 04d5fe6..2349049 100644 --- 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 @@ -1,236 +1,66 @@ package org.oneedtech.inspect.vc.probe; -import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPPER; - -import java.io.ByteArrayOutputStream; import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.security.KeyFactory; -import java.security.MessageDigest; -import java.security.PublicKey; -import java.security.Security; -import java.security.Signature; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Base64.Encoder; -import java.util.Map.Entry; -import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; -import org.bouncycastle.asn1.x509.AlgorithmIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.crypto.Signer; -import org.bouncycastle.crypto.signers.Ed25519Signer; -import org.bouncycastle.crypto.signers.RSADigestSigner; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.util.encoders.Hex; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; -import com.apicatalog.jsonld.JsonLd; -import com.apicatalog.jsonld.StringUtils; import com.apicatalog.jsonld.document.JsonDocument; -import com.apicatalog.jsonld.http.media.MediaType; -import com.apicatalog.rdf.Rdf; -import com.apicatalog.rdf.RdfDataset; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.bitcoin.core.Base58; -import com.google.common.io.BaseEncoding; +import com.apicatalog.ld.DocumentError; +import com.apicatalog.ld.signature.VerificationError; +import com.apicatalog.vc.Vc; +import com.apicatalog.vc.processor.StatusVerifier; -import io.setl.rdf.normalization.RdfNormalize; +import jakarta.json.JsonObject; /** - * A Probe that verifies credential proofs - * @author mlyon + * A Probe that verifies a credential's proof. + * @author mgylling */ public class ProofVerifierProbe extends Probe { + /* + * Note: using com.apicatalog Iron, we get a generic VC verifier that + * will test other stuff than the Proof. So sometimes it may be that + * Iron internally retests something that we're already testing out in the + * Inspector class (e.g. expiration). But use this for now -- and remember + * that this probe is only run if the given credential has internal proof + * (aka is not a jwt). + */ + public ProofVerifierProbe() { super(ID); } @Override - public ReportItems run(Credential crd, RunContext ctx) throws Exception { - + public ReportItems run(Credential crd, RunContext ctx) throws Exception { + JsonDocument jsonDoc = JsonDocument.of(new StringReader(crd.getJson().toString())); + JsonObject json = jsonDoc.getJsonContent().get().asJsonObject(); try { - String document = fetchConanicalDocument(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); - String proof = fetchConanicalProof(crd, C14n.URDNA2015, MediaType.N_QUADS, ctx); - //System.out.println(canonical); - - byte[] docHash = getBytes(document); - byte[] proofHash = getBytes(proof); - // concatenate hash of c14n proof options and hash of c14n document - byte[] combined = mergeArrays(proofHash, docHash); - - boolean test = testSigner(combined); - - //TODO if proofs fail, report OutCome.Fatal - //return fatal("msg", ctx); - - } catch (Exception e) { - return exception(e.getMessage(), crd.getResource()); - } + Vc.verify(json) + .loader(new CachingDocumentLoader()) + .useBundledContexts(false) //we control the cache in the loader + //.statusVerifier(new NoopStatusVerifier()) + //.domain(...) + //.didResolver(...) + .isValid(); + } catch (DocumentError e) { + return error(e.getType() + " " + e.getSubject(), ctx); + } catch (VerificationError e) { + return error(e.getCode().name() + " " + e.getMessage(), ctx); + } return success(ctx); - } - - private String fetchConanicalDocument(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { + } - //clone the incoming credential object so we can modify it freely - ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); - JsonNode copy = mapper.readTree(crd.getJson().toString()); - - //remove proof - ((ObjectNode)copy).remove("proof"); - - //create JSON-P Json-LD instance - JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(copy.toString())); - - //create rdf and normalize - RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).loader(new CachingDocumentLoader()).ordered(true).get(); - RdfDataset normalized = RdfNormalize.normalize(dataSet); - - //serialize to string - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Rdf.createWriter(mediaType, os).write(normalized); - String result = StringUtils.stripTrailing(os.toString()); - - return result; - } - - private String fetchConanicalProof(Credential crd, C14n algo, MediaType mediaType, RunContext ctx) throws Exception { - - //clone the incoming credential object so we can modify it freely - ObjectMapper mapper = (ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER); - JsonNode copy = mapper.readTree(crd.getJson().toString()); - - //Get the context node to stitch back in. - JsonNode context = copy.get("@context"); - - //Pull out and use proof node only - JsonNode proof = copy.get("proof"); - - //TODO: Make this better at discarding all, but the linked data proof method. - if(proof.isArray()){ - proof = proof.get(0); - } - - //remove these if they exist - ((ObjectNode)proof).remove("jwt"); - ((ObjectNode)proof).remove("signatureValue"); - ((ObjectNode)proof).remove("proofValue"); - - JsonNode newNode = mapper.createObjectNode(); - ((ObjectNode) newNode).set("@context", context); - //Try to structure this 'to the letter' per a slack with Markus - //((ObjectNode) newNode).set("proof", proof); - - //So that we don't remove nodes while iterating over it save all nodes - Iterator> iter = proof.fields(); - while (iter.hasNext()) { - Entry next = iter.next(); - ((ObjectNode) newNode).set(next.getKey(), next.getValue()); - } - - //create JSON-P Json-LD instance - JsonDocument jsonLdDoc = JsonDocument.of(new StringReader(newNode.toString())); - - //create rdf and normalize - //RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).ordered(true).get(); - RdfDataset dataSet = JsonLd.toRdf(jsonLdDoc).loader(new CachingDocumentLoader()).ordered(true).get(); - RdfDataset normalized = RdfNormalize.normalize(dataSet); - - //serialize to string - ByteArrayOutputStream os = new ByteArrayOutputStream(); - Rdf.createWriter(mediaType, os).write(normalized); - String result = StringUtils.stripTrailing(os.toString()); - - return result; - } - - private boolean testSigner(byte[] concatBytes) throws Exception { - - - final var provider = new BouncyCastleProvider(); - Security.addProvider(provider); - - //Base 58 decode minus the z - var publicKeyBytes = Base58.decode("6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj"); - //The slice out the first two array elements (???) - byte[] slicedArray = Arrays.copyOfRange(publicKeyBytes, 2, 34); - - - final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), slicedArray); - final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); - final var kf = KeyFactory.getInstance("ed25519", provider); - final var publicKey = kf.generatePublic(pkSpec); - final var signedData = Signature.getInstance("ed25519", provider); - signedData.initVerify(publicKey); - signedData.update(concatBytes); - - var signatureBytes = Base58.decode("3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"); - - return signedData.verify(signatureBytes); - - } - - //An alternate path with bouncy castle - /* - private boolean testSigner3(String message, byte[] concateBytes) throws Exception { - - - // Test case defined in https://www.rfc-editor.org/rfc/rfc8037 - var msg = "eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc".getBytes(StandardCharsets.UTF_8); - var expectedSig = "hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg"; - - var privateKeyBytes = Base64.getUrlDecoder().decode("nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"); - var publicKeyBytes = Base64.getUrlDecoder().decode("11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"); - - var privateKey = new Ed25519PrivateKeyParameters(privateKeyBytes, 0); - var publicKey = new Ed25519PublicKeyParameters(publicKeyBytes, 0); - - // Generate new signature - Signer signer = new Ed25519Signer(); - signer.init(true, privateKey); - signer.update(msg, 0, msg.length); - byte[] signature = signer.generateSignature(); - var actualSignature = Base64.getUrlEncoder().encodeToString(signature).replace("=", ""); - - LOG.info("Expected signature: {}", expectedSig); - LOG.info("Actual signature : {}", actualSignature); - - assertEquals(expectedSig, actualSignature); - - - return true; - } - */ - - - private byte[] getBytes(String value) throws Exception{ - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - return digest.digest( - value.getBytes(StandardCharsets.UTF_8) - ); - } - - private static byte[] mergeArrays(final byte[] array1, byte[] array2) { - byte[] joinedArray = Arrays.copyOf(array1, array1.length + array2.length); - System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); - return joinedArray; + private static final class NoopStatusVerifier implements StatusVerifier { + @Override + public void verify(Status status) throws DocumentError, VerifyError { + // noop + } } - private enum C14n { - URDNA2015 - } - - public static final String ID = ProofVerifierProbe.class.getSimpleName(); + public static final String ID = ProofVerifierProbe.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 index e8c06ae..f209b76 100644 --- 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 @@ -1,22 +1,22 @@ package org.oneedtech.inspect.vc.probe; import static com.google.common.base.Strings.isNullOrEmpty; +import static org.oneedtech.inspect.util.code.Defensives.checkTrue; import java.math.BigInteger; -import java.nio.charset.StandardCharsets; import java.security.KeyFactory; import java.security.PublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.RSAPublicKeySpec; -import java.util.List; import java.util.Base64; import java.util.Base64.Decoder; +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.util.code.Defensives; import org.oneedtech.inspect.vc.Credential; -import org.springframework.util.Base64Utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; @@ -43,25 +43,27 @@ public class SignatureVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { try { - verifySignature(crd); + verifySignature(crd, ctx); } catch (Exception e) { return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); - } - + } return success(ctx); } - private void verifySignature(Credential crd) throws Exception { - DecodedJWT decodedJwt = null; - String jwt = crd.getJwt(); - if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid jwt"); + private void verifySignature(Credential crd, RunContext ctx) throws Exception { + checkTrue(crd.getJwt().isPresent(), "no jwt supplied"); + checkTrue(crd.getJwt().get().length() > 0, "no jwt supplied"); + + DecodedJWT decodedJwt = null; + String jwt = crd.getJwt().get(); + List parts = Splitter.on('.').splitToList(jwt); if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); final Decoder decoder = Base64.getUrlDecoder(); String joseHeader = new String(decoder.decode(parts.get(0))); - ObjectMapper mapper = new ObjectMapper(); + ObjectMapper mapper = ((ObjectMapper)ctx.get(RunContext.Key.JACKSON_OBJECTMAPPER)); JsonNode headerObj = mapper.readTree(joseHeader); //MUST be "RS256" @@ -86,9 +88,13 @@ public class SignatureVerifierProbe extends Probe { String modulusString = jwk.get("n").textValue(); String exponentString = jwk.get("e").textValue(); - BigInteger modulus = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(modulusString)); - BigInteger exponent = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(exponentString)); +// BigInteger modulus = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(modulusString)); +// BigInteger exponent = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(exponentString)); +// mgy: use java util decoder instead of spring? + BigInteger modulus = new BigInteger(1, decoder.decode(modulusString)); + BigInteger exponent = new BigInteger(1, decoder.decode(exponentString)); + PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent)); Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null); diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java new file mode 100644 index 0000000..b079ac2 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java @@ -0,0 +1,55 @@ +package org.oneedtech.inspect.vc.probe; + +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; + +import java.util.List; + +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.util.JsonNodeUtil; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * A Probe that verifies a credential's type property. + * @author mgylling + */ +public class TypePropertyProbe extends Probe { + private final Credential.Type expected; + + public TypePropertyProbe(Credential.Type expected) { + super(ID); + this.expected = checkNotNull(expected); + } + + @Override + public ReportItems run(JsonNode root, RunContext ctx) throws Exception { + + ArrayNode typeNode = (ArrayNode)root.get("type"); + if(typeNode == null) return fatal("No type property", ctx); + + List values = JsonNodeUtil.asStringList(typeNode); + + if(!values.contains("VerifiableCredential")) { + return fatal("The type property does not contain the entry 'VerifiableCredential'", ctx); + } + + if(expected == Credential.Type.OpenBadgeCredential) { + if(!values.contains("OpenBadgeCredential") && !values.contains("AchievementCredential")) { + return fatal( + "The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", + ctx); + } + } else { + //TODO implement + throw new IllegalStateException(); + } + + return success(ctx); + } + + public static final String ID = TypePropertyProbe.class.getSimpleName(); +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index dab8845..2cefa2e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -22,7 +22,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; /** - * A DocumentLoader with a threadsafe static cache. + * A com.apicatalog DocumentLoader with a threadsafe static cache. * @author mgylling */ public class CachingDocumentLoader implements DocumentLoader { @@ -33,7 +33,7 @@ public class CachingDocumentLoader implements DocumentLoader { try { return documentCache.get(tpl); } catch (Exception e) { - logger.error("contextCache not able to load {}", url); + logger.error("documentCache not able to load {}", url); throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage()); } } 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 index 1d71ed7..aaf6083 100644 --- 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 @@ -11,27 +11,27 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; /** - * Node access utilities. + * Json node 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) { + public static List asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) { 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); + ArrayNode array = evaluator.eval(jsonPath, root); + for(JsonNode node : array) { + if(!(node instanceof ArrayNode)) { + list.add(node); + } else { + ArrayNode values = (ArrayNode) node; + for(JsonNode value : values) { + list.add(value); + } } } return list; } - + public static List asStringList(JsonNode node) { if(!(node instanceof ArrayNode)) { return List.of(node.asText()); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 4426250..d5cb9c6 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -1,6 +1,6 @@ package org.oneedtech.inspect.vc; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.*; import static org.oneedtech.inspect.test.Assertions.*; import org.junit.jupiter.api.BeforeAll; @@ -9,12 +9,17 @@ 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.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; +import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; +import org.oneedtech.inspect.vc.probe.ProofVerifierProbe; +import org.oneedtech.inspect.vc.probe.TypePropertyProbe; +import com.google.common.collect.Iterables; public class OB30Tests { private static OB30Inspector validator; - private static boolean verbose = true; + private static boolean verbose = false; @BeforeAll static void setup() { @@ -32,6 +37,7 @@ public class OB30Tests { }); } + @Disabled //TODO @Miles -- this needs update? @Test void testSimplePNGPlainValid() { assertDoesNotThrow(()->{ @@ -50,6 +56,7 @@ public class OB30Tests { }); } + @Disabled //TODO @Miles -- this needs update? @Test void testSimpleJsonSVGPlainValid() { assertDoesNotThrow(()->{ @@ -58,8 +65,7 @@ public class OB30Tests { assertValid(report); }); } - - @Disabled + @Test void testSimpleJsonSVGJWTValid() { assertDoesNotThrow(()->{ @@ -71,20 +77,60 @@ public class OB30Tests { @Test void testSimpleJsonInvalidUnknownType() { + //add a dumb value to .type and remove the ob type assertDoesNotThrow(()->{ Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_UNKNOWN_TYPE.asFileResource()); if(verbose) PrintHelper.print(report, true); assertInvalid(report); + assertFatalCount(report, 1); + assertHasProbeID(report, TypePropertyProbe.ID, true); }); } - + @Test - void testCompleteJsonInvalidInlineSchemaRef() throws Exception { + void testSimpleJsonInvalidProof() { + //add some garbage chars to proofValue assertDoesNotThrow(()->{ - Report report = validator.run(Samples.OB30.JSON.COMPLETE_JSON.asFileResource()); + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_ERROR.asFileResource()); if(verbose) PrintHelper.print(report, true); assertInvalid(report); assertErrorCount(report, 1); + assertHasProbeID(report, ProofVerifierProbe.ID, true); + }); + } + + @Test + void testSimpleJsonExpired() { + //"expirationDate": "2020-01-20T00:00:00Z", + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_EXPIRED.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertHasProbeID(report, ExpirationVerifierProbe.ID, true); + }); + } + + @Test + void testSimpleJsonNotIssued() { + //"issuanceDate": "2040-01-01T00:00:00Z", + //this breaks the proof too + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ISSUED.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertHasProbeID(report, IssuanceVerifierProbe.ID, true); + }); + } + + @Test + void testCompleteJsonInvalidInlineSchemaRef() throws Exception { + //404 inline schema ref, and 404 refresh uri + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.COMPLETE_JSON.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertFalse(report.asBoolean()); + assertTrue(Iterables.size(report.getErrors()) > 0); + assertTrue(Iterables.size(report.getExceptions()) > 0); assertHasProbeID(report, InlineJsonSchemaProbe.ID, true); }); } diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 2356c2e..1e08b0d 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -13,6 +13,9 @@ public class Samples { 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 final static Sample SIMPLE_JSON_PROOF_ERROR = new Sample("ob30/simple-proof-error.json", false); + public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-expired.json", false); + public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-issued.json", false); } public static final class PNG { public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java new file mode 100644 index 0000000..28b8e7c --- /dev/null +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java @@ -0,0 +1,119 @@ +package org.oneedtech.inspect.vc.credential; + +import static org.junit.jupiter.api.Assertions.*; +import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.probe.RunContext.Key; +import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; +import org.oneedtech.inspect.util.json.ObjectMapperCache; +import org.oneedtech.inspect.util.resource.Resource; +import org.oneedtech.inspect.util.resource.ResourceType; +import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.OB30Inspector; +import org.oneedtech.inspect.vc.Samples; +import org.oneedtech.inspect.vc.payload.PayloadParser; +import org.oneedtech.inspect.vc.payload.PayloadParserFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class PayloadParserTests { + + @Test + void testSvgStringExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource(ResourceType.SVG); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + @Test + void testSvgJwtExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + @Test + void testPngStringExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource(ResourceType.PNG); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + @Test + void testPngJwtExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource(ResourceType.PNG); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + @Test + void testJwtExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.JWT.SIMPLE_JWT.asFileResource(ResourceType.JWT); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + @Test + void testJsonExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.JSON.SIMPLE_JSON.asFileResource(ResourceType.JSON); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + Credential crd = ext.parse(res, mockOB30Context(res)); + //System.out.println(crd.getJson().toPrettyString()); + assertNotNull(crd); + assertNotNull(crd.getJson()); + assertNotNull(crd.getJson().get("@context")); + }); + } + + private RunContext mockOB30Context(Resource res) { + ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); + JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); + return new RunContext.Builder() + .put(new OB30Inspector.Builder().build()) + .put(res) + .put(Key.JACKSON_OBJECTMAPPER, mapper) + .put(Key.JSONPATH_EVALUATOR, jsonPath) + .build(); + } +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/JsonNodeUtilTests.java index 909cf89..762bd65 100644 --- 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 @@ -1,6 +1,6 @@ package org.oneedtech.inspect.vc.util; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; import java.util.List; @@ -13,18 +13,31 @@ import org.oneedtech.inspect.vc.Samples; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; public class JsonNodeUtilTests { static final ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); static final JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); - @Test - void getEndorsementsTest() throws Exception { - assertDoesNotThrow(()->{ - JsonNode root = mapper.readTree(Samples.OB30.JSON.COMPLETE_JSON.asBytes()); - List list = JsonNodeUtil.getEndorsements(root, jsonPath); + void testFlattenNodeList() { + Assertions.assertDoesNotThrow(()->{ + String json = Samples.OB30.JSON.COMPLETE_JSON.asString(); + JsonNode root = mapper.readTree(json); + List list = JsonNodeUtil.asNodeList(root, "$..endorsement", jsonPath); Assertions.assertEquals(5, list.size()); - }); + for(JsonNode node : list) { + ArrayNode types = (ArrayNode) node.get("type"); + boolean found = false; + for(JsonNode val : types) { + if(val.asText().equals("EndorsementCredential")) { + found = true; + } + } + assertTrue(found); + } + + }); } + } diff --git a/inspector-vc/src/test/resources/ob30/simple-expired.json b/inspector-vc/src/test/resources/ob30/simple-expired.json new file mode 100644 index 0000000..80c6959 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-expired.json @@ -0,0 +1,37 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "expirationDate": "2020-01-20T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file diff --git a/inspector-vc/src/test/resources/ob30/simple-issued.json b/inspector-vc/src/test/resources/ob30/simple-issued.json new file mode 100644 index 0000000..e129e3d --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-issued.json @@ -0,0 +1,37 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2040-01-01T00:00:00Z", + "expirationDate": "2050-01-20T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file diff --git a/inspector-vc/src/test/resources/ob30/simple-proof-error.json b/inspector-vc/src/test/resources/ob30/simple-proof-error.json new file mode 100644 index 0000000..f749e52 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-proof-error.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-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "XXXz3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnMXXX" + } + ] +} \ No newline at end of file From ec689cc1102c400a2fb80349f4dfcdf3cbee0fe1 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 8 Jul 2022 12:25:47 +0200 Subject: [PATCH 19/58] update pom --- inspector-vc/pom.xml | 2 ++ .../src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 23022ba..83979cf 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -28,12 +28,14 @@ java-jwt 3.10.3 + com.apicatalog iron-verifiable-credentials-jre8 0.7.0 + com.apicatalog titanium-json-ld diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index a6518fd..15683d5 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -138,7 +138,7 @@ public class OB30Inspector extends VCInspector { //embedded endorsements EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); - List endorsements = JsonNodeUtil.asNodeList(crd.getJson(), "$..endorsement", jsonPath); + List endorsements = asNodeList(crd.getJson(), "$..endorsement", jsonPath); for(JsonNode node : endorsements) { probeCount++; Credential endorsement = new Credential(resource, node); @@ -146,7 +146,7 @@ public class OB30Inspector extends VCInspector { } //embedded jwt endorsements - endorsements = JsonNodeUtil.asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); + endorsements = asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); for(JsonNode node : endorsements) { probeCount++; String jwt = node.asText(); From 2b6cf3311fe9872f341034f282140701419e15a7 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 8 Jul 2022 10:03:49 -0400 Subject: [PATCH 20/58] Updated testing SVG with a credential containing a valid proof. --- .../src/test/java/org/oneedtech/inspect/vc/OB30Tests.java | 1 - inspector-vc/src/test/resources/ob30/simple-json.svg | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index d5cb9c6..fb41a70 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -56,7 +56,6 @@ public class OB30Tests { }); } - @Disabled //TODO @Miles -- this needs update? @Test void testSimpleJsonSVGPlainValid() { assertDoesNotThrow(()->{ diff --git a/inspector-vc/src/test/resources/ob30/simple-json.svg b/inspector-vc/src/test/resources/ob30/simple-json.svg index 90e4219..8310e1d 100644 --- a/inspector-vc/src/test/resources/ob30/simple-json.svg +++ b/inspector-vc/src/test/resources/ob30/simple-json.svg @@ -32,10 +32,10 @@ "proof": [ { "type": "Ed25519Signature2020", - "created": "2022-06-09T22:56:28Z", - "verificationMethod": "https://example.edu/issuers/565049#key-1", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", "proofPurpose": "assertionMethod", - "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN" + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" } ] } From 0f41d00a0010732f051642128af41b0f9dc88844 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 8 Jul 2022 10:31:33 -0400 Subject: [PATCH 21/58] Rebaked simple PNG image with latest example to fix broken unit tests. --- .../org/oneedtech/inspect/vc/OB30Tests.java | 1 - .../src/test/resources/ob30/simple-json.png | Bin 83758 -> 83704 bytes 2 files changed, 1 deletion(-) diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index fb41a70..2ea5cf4 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -37,7 +37,6 @@ public class OB30Tests { }); } - @Disabled //TODO @Miles -- this needs update? @Test void testSimplePNGPlainValid() { assertDoesNotThrow(()->{ diff --git a/inspector-vc/src/test/resources/ob30/simple-json.png b/inspector-vc/src/test/resources/ob30/simple-json.png index b424db95afd9a5d935c8b9a128c7f78b2b88dd75..f13d2447a0ec9fcc5314ac7d7bedcec723b3b560 100644 GIT binary patch delta 468 zcmZ42$NHm}wP6e6H77>i>DQeY75J1KlJoOQQY%W7tQ4ZTrgJ(o8cnx#X7rri;ml~t z9IL}M{fIN8p_)=jWkD)fi2@KPg{2l{re!83<)k_nrKY6jm1HL7C;`GuHwcw_O-@rgORi z6&krRN>BH9Wi*}s-I-C)$T2x1Gqo%=7id>-X;M~datTy7&D!i`k7)gx|;_#L&#j$im9lEDGdI4L3$v>y*qCtL)TDt12_!?Cek%V=qIKyzI!T zJQK6P)Y8PT;JgC!6gLagKriod?=r8lwBoD^bIZhtERX?3ZjAc%5F5i1b4o!Wqf}+= z8(Lx%RT^sHR9To=R*_-mUXbDHYi8=`7Fp?+=pLDwlxt~T;S-XW=4)Q+UYZsa=mRu3 z$Re{Os=UIxIJ>OG&DYpDw4~C+tjx&FB`Pz;*DpUX&llp+TA&|exoUqK-gwCX1fH&J KelF{r5}E+2ON=A{ delta 521 zcmey-%etr>DQeY6@(NNlpK=t^GZ@HN|dY=qPY|lrn5RT8cjEKX7rw3 z=ger%uAmUB0}|fh%xI_sR9;e9kP20*00c^5sYRJ-nTbg`sm?{IDXDoSnTa_{U>*D} zj56X-?LdW@#l@wmML@09AmeOY7)>-WjRT4V6y>L7=Aof(_p8 z!l*NSvJ0c^^nWglJkvQ`fh=WLM)N?d<~b&3WTuv-<^r7-T$+@Xnp^@m8f*pFOrXw! zqWt_cB%gtuIoFj@M;$1cT$Gwvl9~dPRx&a$GSW3L(>1URF*33;HM26Zh=O?d4agzx zE{sat%Gs%vx`q%@7dJ-zdYJiPi8-a9;8UtHwa85Ma?UWx_A^U$H3};<38{<>4e}0l zHpvJNcl9<;GB@?}3-vK7NsRK%@GdM5DM>ALs&qFj56X9UNpUVL^z#WfbuTnb$qdaj pDXfe#GcE}XDyj5?d9D`hxmX}sn`^`;HH85PJYD@<);T3K0RT=ZiO~Q6 From 9ab087f02681f9395cd1816edc1aee740d78b078 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 8 Jul 2022 10:51:18 -0400 Subject: [PATCH 22/58] Added in code to fetch jwk from a url. --- inspector-vc/pom.xml | 6 +++ .../vc/probe/SignatureVerifierProbe.java | 43 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 83979cf..2a1342f 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -41,6 +41,12 @@ titanium-json-ld 1.3.1 + + + org.apache.httpcomponents + httpclient + 4.5.13 + diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 15683d5..75b7a74 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -82,7 +82,7 @@ public class OB30Inspector extends VCInspector { List accumulator = new ArrayList<>(); int probeCount = 0; - try { + try { //detect type (png, svg, json, jwt) and extract json data probeCount++; accumulator.add(new CredentialParseProbe().run(resource, ctx)); From 81b133ec8405060ea2c68ce7cda3f6f6e03e5910 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Tue, 19 Jul 2022 10:13:33 -0400 Subject: [PATCH 28/58] Basline simple.json with inspector and one test. --- inspector-vc/pom.xml | 4 ---- .../src/test/java/org/oneedtech/inspect/vc/Samples.java | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 516e996..538e795 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -17,10 +17,6 @@ com.fasterxml.jackson.core jackson-databind - - com.jayway.jsonpath - json-path - com.networknt json-schema-validator diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index b0904bc..07481a1 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -3,7 +3,6 @@ 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); @@ -26,4 +25,10 @@ public class Samples { public final static Sample SIMPLE_JWT = new Sample("ob30/simple.jwt", true); } } + + public static final class CLR20 { + public static final class JSON { + public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true); + } + } } From 0c855ec71e9a24656d4acb51ac8f1bd71dedb4fd Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Tue, 19 Jul 2022 15:51:44 -0400 Subject: [PATCH 29/58] WIP, CLR Inspector. Inline Schema probe having an issue that I need to dig deeper into. --- .../main/java/org/oneedtech/inspect/vc/Credential.java | 5 +++++ .../oneedtech/inspect/vc/probe/TypePropertyProbe.java | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) 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 index e64ac98..e88ab1b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -70,6 +70,8 @@ public class Credential extends GeneratedObject { return Optional.of(Catalog.OB_30_VERIFIABLEPRESENTATION_JSON); } else if(credentialType == Credential.Type.EndorsementCredential) { return Optional.of(Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON); + } else if(credentialType == Credential.Type.ClrCredential) { + return Optional.of(Catalog.CLR_20_CLRCREDENTIAL_JSON); } return Optional.empty(); } @@ -77,6 +79,7 @@ public class Credential extends GeneratedObject { public enum Type { AchievementCredential, OpenBadgeCredential, //treated as an alias of AchievementCredential + ClrCredential, //NOT a duplicate of OB this does not use an alias and we ONLY use 'ClrCredential' as the base type EndorsementCredential, VerifiablePresentation, VerifiableCredential, //this is an underspecifier in our context @@ -89,6 +92,8 @@ public class Credential extends GeneratedObject { String value = iter.next().asText(); if(value.equals("AchievementCredential") || value.equals("OpenBadgeCredential")) { return AchievementCredential; + } else if(value.equals("ClrCredential")) { + return ClrCredential; } else if(value.equals("VerifiablePresentation")) { return VerifiablePresentation; } else if(value.equals("EndorsementCredential")) { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java index b079ac2..078817b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java @@ -8,6 +8,7 @@ import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.Credential.Type; import org.oneedtech.inspect.vc.util.JsonNodeUtil; import com.fasterxml.jackson.databind.JsonNode; @@ -42,7 +43,14 @@ public class TypePropertyProbe extends Probe { return fatal( "The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", ctx); - } + } + } + if(expected == Credential.Type.ClrCredential){ + if(!values.contains("ClrCredential")) { + return fatal( + "The type property does not contain the entry 'ClrCredential'", + ctx); + } } else { //TODO implement throw new IllegalStateException(); From c42ca86915a029aa284d7a77fd20106ed74202a4 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Sun, 31 Jul 2022 14:19:29 -0700 Subject: [PATCH 30/58] WIP, but need to hash out with Markus a few things. --- .../oneedtech/inspect/vc/EmbeddedVCInspector.java | 13 +++++++++++++ .../test/java/org/oneedtech/inspect/vc/Samples.java | 1 + 2 files changed, 14 insertions(+) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java new file mode 100644 index 0000000..8d79ce4 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java @@ -0,0 +1,13 @@ +package org.oneedtech.inspect.vc; + +import java.util.Map; + +import org.oneedtech.inspect.core.SubInspector; +import org.oneedtech.inspect.core.report.Report; +import org.oneedtech.inspect.util.resource.Resource; + +public class EmbeddedVCInspector { + //TODO: @Miles Need to confirm with Markus, but this feels like an embedded inspector with some different rules (type, etc..) like endorsements + + +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 07481a1..6b6df50 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -29,6 +29,7 @@ public class Samples { public static final class CLR20 { public static final class JSON { public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true); + public final static Sample SIMPLE_JWT = new Sample("clr20/simple.jwt", true); } } } From a8ffcaae37b223db08fe18941bec1bdcddfbd545 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Tue, 2 Aug 2022 15:09:52 -0400 Subject: [PATCH 31/58] Undid POM exclusions and commented out mvn enforcer to address issues with missing runtime jars. --- inspector-vc/pom.xml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 538e795..d0c5c65 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -12,16 +12,6 @@ org.1edtech inspector-core - - - com.fasterxml.jackson.core - jackson-databind - - - com.networknt - json-schema-validator - - com.auth0 From 0545dbf9e32b2db1e2c9b33f996196e883cc55f4 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Tue, 9 Aug 2022 14:49:54 -0400 Subject: [PATCH 32/58] Introduced an OB validation error while adding CLR. Fixed a missing 'else if' --- .../java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java index 078817b..3c17f25 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java @@ -45,7 +45,7 @@ public class TypePropertyProbe extends Probe { ctx); } } - if(expected == Credential.Type.ClrCredential){ + else if(expected == Credential.Type.ClrCredential){ if(!values.contains("ClrCredential")) { return fatal( "The type property does not contain the entry 'ClrCredential'", From 79b09325292e9913025011103fe69e8f600e74c0 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 12 Aug 2022 15:04:53 -0400 Subject: [PATCH 33/58] Filling out embedded verifiable credential inspector. --- .../inspect/vc/EmbeddedVCInspector.java | 157 +++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java index 8d79ce4..5488048 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java @@ -1,13 +1,164 @@ package org.oneedtech.inspect.vc; +import static java.lang.Boolean.TRUE; + +import static org.oneedtech.inspect.core.probe.RunContext.Key.*; +import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Optional; import org.oneedtech.inspect.core.SubInspector; +import org.oneedtech.inspect.core.probe.GeneratedObject; +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; +import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; +import org.oneedtech.inspect.core.report.ReportItems; +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.UriResource; +import org.oneedtech.inspect.util.resource.context.ResourceContext; +import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.CredentialParseProbe; +import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; +import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; +import org.oneedtech.inspect.vc.probe.TypePropertyProbe; -public class EmbeddedVCInspector { - //TODO: @Miles Need to confirm with Markus, but this feels like an embedded inspector with some different rules (type, etc..) like endorsements +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; - +/** + * An inspector for EndorsementCredential objects. + * @author mgylling + */ +public class EmbeddedVCInspector extends VCInspector implements SubInspector { + + protected > EmbeddedVCInspector(B builder) { + super(builder); + } + + @Override + public Report run(Resource resource, Map parentObjects) { + /* + * The resource param is the top-level credential that embeds the endorsement, we + * expect parentObjects to provide a pointer to the JsonNode we should check + */ + Credential verifiableCredential = (Credential) parentObjects.get(VC_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(VC_KEY, verifiableCredential) + .build(); + + List accumulator = new ArrayList<>(); + int probeCount = 0; + + try { + //detect type (png, svg, json, jwt) and extract json data + probeCount++; + accumulator.add(new CredentialParseProbe().run(resource, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //we expect the above to place a generated object in the context + Credential crd = ctx.getGeneratedObject(Credential.ID); + + //type property + probeCount++; + accumulator.add(new TypePropertyProbe(Type.ClrCredential).run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //canonical schema and inline schema + SchemaKey schema = crd.getSchemaKey().orElseThrow(); + for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { + probeCount++; + accumulator.add(probe.run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //signatures, proofs + probeCount++; + if(crd.getJwt().isPresent()){ + //The credential originally contained in a JWT, validate the jwt and external proof. + accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + } else { + //The credential not contained in a jwt, must have an internal proof. + //TODO: @Miles Need to fix the issuer, Same as with outer CLR + //Swap -> "verificationMethod": "https://example.edu/issuers/565049#z6MkwA1498JfoCS3y4y3zggBDAosQEoCi5gsYH2PMXh1cFWK", + //To be like -> "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + //...but also work properly which old record seems not be doing... + + /* + 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); + if(newID.isPresent()) { + //TODO resource.type + return this.run( + new UriResource(new URI(newID.get())) + .setContext(new ResourceContext(REFRESHED, TRUE))); + } + } + + } catch (Exception e) { + //TODO: Need to figure out the issue here. + //accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); + } + + return new Report(ctx, new ReportItems(accumulator), probeCount); + } + + @Override + public Report run(R resource) { + throw new IllegalStateException("must use #run(resource, map)"); + } + + public static class Builder extends VCInspector.Builder { + @SuppressWarnings("unchecked") + @Override + public EmbeddedVCInspector build() { + return new EmbeddedVCInspector(this); + } + } + + public static final String VC_KEY = "VC_KEY"; + private static final String REFRESHED = "is.refreshed.credential"; + + /** + * 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) { + JsonNode refreshServiceNode = crd.getJson().get("refreshService"); + if(refreshServiceNode != null) { + JsonNode serviceTypeNode = refreshServiceNode.get("type"); + if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { + JsonNode serviceURINode = refreshServiceNode.get("id"); + if(serviceURINode != null) { + return Optional.of(serviceURINode.asText()); + } + } + } + return Optional.empty(); + } } From a7402e5a73b32d3adac12c4205003c16a178ff83 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 12 Aug 2022 16:09:17 -0400 Subject: [PATCH 34/58] Everything except endorsements, fixing CLR sample with proof and a few sanity checks. --- .../inspect/vc/EmbeddedVCInspector.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java index 5488048..7590d7f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java @@ -4,6 +4,7 @@ import static java.lang.Boolean.TRUE; import static org.oneedtech.inspect.core.probe.RunContext.Key.*; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; +import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; import java.net.URI; import java.util.ArrayList; @@ -26,7 +27,10 @@ import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.vc.Credential.Type; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; +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.RevocationListProbe; import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; @@ -106,7 +110,7 @@ public class EmbeddedVCInspector extends VCInspector implements SubInspector { } if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - //check refresh service if we are not already refreshed + //check refresh service if we are not already refreshed (check just like in external CLR) probeCount++; if(resource.getContext().get(REFRESHED) != TRUE) { Optional newID = checkRefreshService(crd, ctx); @@ -117,10 +121,21 @@ public class EmbeddedVCInspector extends VCInspector implements SubInspector { .setContext(new ResourceContext(REFRESHED, TRUE))); } } + + //revocation, expiration and issuance (check just like in external CLR) + for(Probe probe : List.of(new RevocationListProbe(), + new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { + probeCount++; + accumulator.add(probe.run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //TODO: verify embedded endorsements, I believe these are NOT on embedded credentials + + //TODO: verify if User Probes are relevant for embedded content } catch (Exception e) { - //TODO: Need to figure out the issue here. - //accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); + accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); } return new Report(ctx, new ReportItems(accumulator), probeCount); From 469d60eae30ee72bce86119a6006a11457d15fd1 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Mon, 15 Aug 2022 14:22:32 -0400 Subject: [PATCH 35/58] Added endorsements, but need to generate more examples, fix proofs and add more unit tests to confirm I am grabbing the right schemas, etc... --- .../inspect/vc/EmbeddedVCInspector.java | 4 - .../inspect/vc/EndorsementInspector.java | 117 +++++++++++++++++- 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java index 7590d7f..4a5a16f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java @@ -130,10 +130,6 @@ public class EmbeddedVCInspector extends VCInspector implements SubInspector { if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } - //TODO: verify embedded endorsements, I believe these are NOT on embedded credentials - - //TODO: verify if User Probes are relevant for embedded content - } catch (Exception e) { accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); } 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 index 37c84af..9f5f351 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -1,19 +1,40 @@ package org.oneedtech.inspect.vc; +import static java.lang.Boolean.TRUE; + import static org.oneedtech.inspect.core.probe.RunContext.Key.*; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; +import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Optional; import org.oneedtech.inspect.core.SubInspector; import org.oneedtech.inspect.core.probe.GeneratedObject; +import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; +import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.ReportItems; +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.UriResource; +import org.oneedtech.inspect.util.resource.context.ResourceContext; +import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.CredentialParseProbe; +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.RevocationListProbe; +import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; +import org.oneedtech.inspect.vc.probe.TypePropertyProbe; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** @@ -32,7 +53,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector { * The resource param is the top-level credential that embeds the endorsement, we * expect parentObjects to provide a pointer to the JsonNode we should check */ - Credential endorsement = (Credential) parentObjects.get(ENDORSEMENT_KEY); + Credential verifiableCredential = (Credential) parentObjects.get(ENDORSEMENT_KEY); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); @@ -42,12 +63,78 @@ public class EndorsementInspector extends VCInspector implements SubInspector { .put(resource) .put(JACKSON_OBJECTMAPPER, mapper) .put(JSONPATH_EVALUATOR, jsonPath) - .put(ENDORSEMENT_KEY, endorsement) + .put(ENDORSEMENT_KEY, verifiableCredential) .build(); + + List accumulator = new ArrayList<>(); + int probeCount = 0; + + try { + //detect type (png, svg, json, jwt) and extract json data + probeCount++; + accumulator.add(new CredentialParseProbe().run(resource, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //we expect the above to place a generated object in the context + Credential crd = ctx.getGeneratedObject(Credential.ID); + + //type property + probeCount++; + accumulator.add(new TypePropertyProbe(Type.ClrCredential).run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - System.err.println("TODO" + endorsement.toString()); - - return new Report(ctx, new ReportItems(), 1); //TODO + //canonical schema and inline schema + SchemaKey schema = crd.getSchemaKey().orElseThrow(); + for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { + probeCount++; + accumulator.add(probe.run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //signatures, proofs + probeCount++; + if(crd.getJwt().isPresent()){ + //The credential originally contained in a JWT, validate the jwt and external proof. + accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + } else { + //The credential not contained in a jwt, must have an internal proof. + //TODO: @Miles Need to fix the issuer, Same as with outer CLR + //Swap -> "verificationMethod": "https://example.edu/issuers/565049#z6MkwA1498JfoCS3y4y3zggBDAosQEoCi5gsYH2PMXh1cFWK", + //To be like -> "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + //...but also work properly which old record seems not be doing... + + /* + accumulator.add(new ProofVerifierProbe().run(crd, ctx)); + */ + + } + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //check refresh service if we are not already refreshed (check just like in external CLR) + probeCount++; + if(resource.getContext().get(REFRESHED) != TRUE) { + Optional newID = checkRefreshService(crd, ctx); + if(newID.isPresent()) { + //TODO resource.type + return this.run( + new UriResource(new URI(newID.get())) + .setContext(new ResourceContext(REFRESHED, TRUE))); + } + } + + //revocation, expiration and issuance (check just like in external CLR) + for(Probe probe : List.of(new RevocationListProbe(), + new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { + probeCount++; + accumulator.add(probe.run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + } catch (Exception e) { + accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); + } + + return new Report(ctx, new ReportItems(accumulator), probeCount); } @Override @@ -64,5 +151,25 @@ public class EndorsementInspector extends VCInspector implements SubInspector { } public static final String ENDORSEMENT_KEY = "ENDORSEMENT_KEY"; + private static final String REFRESHED = "is.refreshed.credential"; + /** + * 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) { + JsonNode refreshServiceNode = crd.getJson().get("refreshService"); + if(refreshServiceNode != null) { + JsonNode serviceTypeNode = refreshServiceNode.get("type"); + if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { + JsonNode serviceURINode = refreshServiceNode.get("id"); + if(serviceURINode != null) { + return Optional.of(serviceURINode.asText()); + } + } + } + return Optional.empty(); + } } From 7c7d2835304b4d48a7821f7a77fc1e376dd6f26d Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 15:04:32 +0200 Subject: [PATCH 36/58] clean up filenames --- .../oneedtech/inspect/vc/OB30Inspector.java | 7 +- .../org/oneedtech/inspect/vc/IronTests.java | 64 +++++++++++++++++++ .../org/oneedtech/inspect/vc/Samples.java | 12 ++-- ...e-expired.json => simple-err-expired.json} | 0 ...ple-issued.json => simple-err-issued.json} | 0 ...ple-issuer.json => simple-err-issuer.json} | 0 ...proof-error.json => simple-err-proof.json} | 0 ...unknown-type.json => simple-err-type.json} | 0 .../test/resources/ob30/simple-noproof.json | 27 ++++++++ 9 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java rename inspector-vc/src/test/resources/ob30/{simple-expired.json => simple-err-expired.json} (100%) rename inspector-vc/src/test/resources/ob30/{simple-issued.json => simple-err-issued.json} (100%) rename inspector-vc/src/test/resources/ob30/{simple-issuer.json => simple-err-issuer.json} (100%) rename inspector-vc/src/test/resources/ob30/{simple-proof-error.json => simple-err-proof.json} (100%) rename inspector-vc/src/test/resources/ob30/{simple-unknown-type.json => simple-err-type.json} (100%) create mode 100644 inspector-vc/src/test/resources/ob30/simple-noproof.json diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 75b7a74..13e58ac 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -39,7 +39,6 @@ import org.oneedtech.inspect.vc.probe.RevocationListProbe; import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; -import org.oneedtech.inspect.vc.util.JsonNodeUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -90,7 +89,11 @@ public class OB30Inspector extends VCInspector { //we expect the above to place a generated object in the context Credential crd = ctx.getGeneratedObject(Credential.ID); - + + //TODO check context IRIs? the schema doesnt do this + + //TODO new check: that subject @id or IdentityObject is available (at least one is the req) + //type property probeCount++; accumulator.add(new TypePropertyProbe(OpenBadgeCredential).run(crd.getJson(), ctx)); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java new file mode 100644 index 0000000..89db3e0 --- /dev/null +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java @@ -0,0 +1,64 @@ +package org.oneedtech.inspect.vc; + +import java.net.URI; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import com.apicatalog.ld.signature.ed25519.Ed25519Proof2020Adapter; +import com.apicatalog.ld.signature.key.KeyPair; +import com.apicatalog.ld.signature.proof.ProofOptions; +import com.apicatalog.ld.signature.proof.VerificationMethod; +import com.apicatalog.vc.Vc; +import com.apicatalog.vc.processor.Issuer; + +import jakarta.json.JsonObject; + +public class IronTests { + + @Disabled + @Test + void testOb_01() { + Assertions.assertDoesNotThrow(()->{ + URI unsigned = Samples.OB30.JSON.SIMPLE_JSON_NOPROOF.asURL().toURI(); + KeyPair kp = Vc.generateKeys("https://w3id.org/security#Ed25519Signature2020").get(URI.create("urn:1"), 256); + ProofOptions options = ProofOptions.create( + Ed25519Proof2020Adapter.TYPE, + new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")), + URI.create("https://w3id.org/security#assertionMethod")).created(Instant.now().truncatedTo(ChronoUnit.SECONDS)); + + Issuer issuer = Vc.sign(unsigned, kp, options); + JsonObject signed = issuer.getCompacted(); + JsonObject proof = signed.getJsonObject("sec:proof"); + + Assertions.assertNotNull(proof); + + System.err.println (issuer.getCompacted().toString()); + + Vc.verify(issuer.getCompacted()).isValid(); + }); + } + + @Disabled + @Test + void testClr_01() { + Assertions.assertDoesNotThrow(()->{ + URI unsigned = Samples.CLR20.JSON.SIMPLE_JSON_NOPROOF.asURL().toURI(); + KeyPair kp = Vc.generateKeys("https://w3id.org/security#Ed25519Signature2020").get(URI.create("urn:1"), 256); + ProofOptions options = ProofOptions.create( + Ed25519Proof2020Adapter.TYPE, + new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")), + URI.create("https://w3id.org/security#assertionMethod")); + + Issuer issuer = Vc.sign(unsigned, kp, options); + JsonObject job = issuer.getCompacted().getJsonObject("sec:proof"); + + //System.err.println (issuer.getCompacted().toString()); + Assertions.assertNotNull(job); + Vc.verify(issuer.getCompacted()).isValid(); + }); + } +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 6b6df50..0da143e 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -11,11 +11,12 @@ public class Samples { 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 final static Sample SIMPLE_JSON_PROOF_ERROR = new Sample("ob30/simple-proof-error.json", false); - public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-expired.json", false); - public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-issued.json", false); - public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-issuer.json", false); + public final static Sample SIMPLE_JSON_NOPROOF = new Sample("ob30/simple-noproof.json", false); + public final static Sample SIMPLE_JSON_UNKNOWN_TYPE = new Sample("ob30/simple-err-type.json", false); + public final static Sample SIMPLE_JSON_PROOF_ERROR = new Sample("ob30/simple-err-proof.json", false); + public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-err-expired.json", false); + public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false); + public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false); } public static final class PNG { public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true); @@ -29,6 +30,7 @@ public class Samples { public static final class CLR20 { public static final class JSON { public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true); + public final static Sample SIMPLE_JSON_NOPROOF = new Sample("clr20/simple-noproof.json", true); public final static Sample SIMPLE_JWT = new Sample("clr20/simple.jwt", true); } } diff --git a/inspector-vc/src/test/resources/ob30/simple-expired.json b/inspector-vc/src/test/resources/ob30/simple-err-expired.json similarity index 100% rename from inspector-vc/src/test/resources/ob30/simple-expired.json rename to inspector-vc/src/test/resources/ob30/simple-err-expired.json diff --git a/inspector-vc/src/test/resources/ob30/simple-issued.json b/inspector-vc/src/test/resources/ob30/simple-err-issued.json similarity index 100% rename from inspector-vc/src/test/resources/ob30/simple-issued.json rename to inspector-vc/src/test/resources/ob30/simple-err-issued.json diff --git a/inspector-vc/src/test/resources/ob30/simple-issuer.json b/inspector-vc/src/test/resources/ob30/simple-err-issuer.json similarity index 100% rename from inspector-vc/src/test/resources/ob30/simple-issuer.json rename to inspector-vc/src/test/resources/ob30/simple-err-issuer.json diff --git a/inspector-vc/src/test/resources/ob30/simple-proof-error.json b/inspector-vc/src/test/resources/ob30/simple-err-proof.json similarity index 100% rename from inspector-vc/src/test/resources/ob30/simple-proof-error.json rename to inspector-vc/src/test/resources/ob30/simple-err-proof.json diff --git a/inspector-vc/src/test/resources/ob30/simple-unknown-type.json b/inspector-vc/src/test/resources/ob30/simple-err-type.json similarity index 100% rename from inspector-vc/src/test/resources/ob30/simple-unknown-type.json rename to inspector-vc/src/test/resources/ob30/simple-err-type.json diff --git a/inspector-vc/src/test/resources/ob30/simple-noproof.json b/inspector-vc/src/test/resources/ob30/simple-noproof.json new file mode 100644 index 0000000..0496844 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-noproof.json @@ -0,0 +1,27 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + } +} \ No newline at end of file From ef74f679fc79aa68ba0a95bf0dcb092edac17108 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 16:57:12 +0200 Subject: [PATCH 37/58] Update IronTests.java --- .../org/oneedtech/inspect/vc/IronTests.java | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java index 89db3e0..02258fb 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/IronTests.java @@ -1,13 +1,17 @@ package org.oneedtech.inspect.vc; +import java.io.StringWriter; import java.net.URI; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import com.apicatalog.did.key.DidKey; import com.apicatalog.ld.signature.ed25519.Ed25519Proof2020Adapter; import com.apicatalog.ld.signature.key.KeyPair; import com.apicatalog.ld.signature.proof.ProofOptions; @@ -15,7 +19,11 @@ import com.apicatalog.ld.signature.proof.VerificationMethod; import com.apicatalog.vc.Vc; import com.apicatalog.vc.processor.Issuer; +import jakarta.json.Json; import jakarta.json.JsonObject; +import jakarta.json.JsonWriter; +import jakarta.json.JsonWriterFactory; +import jakarta.json.stream.JsonGenerator; public class IronTests { @@ -23,25 +31,30 @@ public class IronTests { @Test void testOb_01() { Assertions.assertDoesNotThrow(()->{ + + + final DidKey didKey = DidKey.from(URI.create("did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH")); + + //https://w3id.org/security#Ed25519KeyPair2020 + //https://w3id.org/security#Ed25519Signature2020 URI unsigned = Samples.OB30.JSON.SIMPLE_JSON_NOPROOF.asURL().toURI(); KeyPair kp = Vc.generateKeys("https://w3id.org/security#Ed25519Signature2020").get(URI.create("urn:1"), 256); ProofOptions options = ProofOptions.create( Ed25519Proof2020Adapter.TYPE, - new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")), + //new VerificationMethod(URI.create("did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj")), + new VerificationMethod(didKey.toUri()), URI.create("https://w3id.org/security#assertionMethod")).created(Instant.now().truncatedTo(ChronoUnit.SECONDS)); Issuer issuer = Vc.sign(unsigned, kp, options); - JsonObject signed = issuer.getCompacted(); - JsonObject proof = signed.getJsonObject("sec:proof"); - - Assertions.assertNotNull(proof); - - System.err.println (issuer.getCompacted().toString()); - + System.err.println(pretty(issuer.getCompacted())); + JsonObject signed = issuer.getCompacted(); + JsonObject proof = signed.getJsonObject("sec:proof"); + Assertions.assertNotNull(proof); + Vc.verify(issuer.getCompacted()).isValid(); }); } - + @Disabled @Test void testClr_01() { @@ -56,9 +69,20 @@ public class IronTests { Issuer issuer = Vc.sign(unsigned, kp, options); JsonObject job = issuer.getCompacted().getJsonObject("sec:proof"); - //System.err.println (issuer.getCompacted().toString()); + //System.err.println (pretty(issuer.getCompacted().toString())); Assertions.assertNotNull(job); Vc.verify(issuer.getCompacted()).isValid(); }); } + + String pretty(JsonObject jobj) { + Map properties = new HashMap<>(1); + properties.put(JsonGenerator.PRETTY_PRINTING, true); + StringWriter sw = new StringWriter(); + JsonWriterFactory writerFactory = Json.createWriterFactory(properties); + JsonWriter jsonWriter = writerFactory.createWriter(sw); + jsonWriter.writeObject(jobj); + jsonWriter.close(); + return sw.toString(); + } } From 5e6a277185fb7fefe5551d31a050852fa54905b8 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 19:48:29 +0200 Subject: [PATCH 38/58] clean up endorsementinspector --- .../inspect/vc/EndorsementInspector.java | 62 +++++++------------ .../oneedtech/inspect/vc/OB30Inspector.java | 4 +- .../inspect/vc/probe/ProofVerifierProbe.java | 2 +- .../inspect/vc/probe/TypePropertyProbe.java | 55 ++++++++-------- 4 files changed, 53 insertions(+), 70 deletions(-) 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 index 9f5f351..9801fc7 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -1,10 +1,10 @@ package org.oneedtech.inspect.vc; import static java.lang.Boolean.TRUE; - import static org.oneedtech.inspect.core.probe.RunContext.Key.*; -import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; +import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; import java.net.URI; import java.util.ArrayList; @@ -17,19 +17,17 @@ import org.oneedtech.inspect.core.probe.GeneratedObject; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; -import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.ReportItems; -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.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.vc.Credential.Type; -import org.oneedtech.inspect.vc.probe.CredentialParseProbe; 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.ProofVerifierProbe; import org.oneedtech.inspect.vc.probe.RevocationListProbe; import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; @@ -49,63 +47,47 @@ public class EndorsementInspector extends VCInspector implements SubInspector { @Override public Report run(Resource resource, Map parentObjects) { + /* * The resource param is the top-level credential that embeds the endorsement, we - * expect parentObjects to provide a pointer to the JsonNode we should check + * expect parentObjects to provide a pointer to the JsonNode we should check. + * + * The parent inspector is responsible to decode away possible jwt-ness, so that + * what we get here is a verbatim json node. + * */ - Credential verifiableCredential = (Credential) parentObjects.get(ENDORSEMENT_KEY); + Credential endorsement = (Credential) checkNotNull(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, verifiableCredential) .build(); List accumulator = new ArrayList<>(); int probeCount = 0; - try { - //detect type (png, svg, json, jwt) and extract json data - probeCount++; - accumulator.add(new CredentialParseProbe().run(resource, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - - //we expect the above to place a generated object in the context - Credential crd = ctx.getGeneratedObject(Credential.ID); - + //type property probeCount++; - accumulator.add(new TypePropertyProbe(Type.ClrCredential).run(crd.getJson(), ctx)); + accumulator.add(new TypePropertyProbe(Type.EndorsementCredential).run(endorsement.getJson(), ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - //canonical schema and inline schema - SchemaKey schema = crd.getSchemaKey().orElseThrow(); - for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { - probeCount++; - accumulator.add(probe.run(crd.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - + //inline schema (parent inspector has already validated against canonical) + accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx)); + //signatures, proofs probeCount++; - if(crd.getJwt().isPresent()){ + if(endorsement.getJwt().isPresent()){ //The credential originally contained in a JWT, validate the jwt and external proof. - accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + accumulator.add(new SignatureVerifierProbe().run(endorsement, ctx)); } else { //The credential not contained in a jwt, must have an internal proof. - //TODO: @Miles Need to fix the issuer, Same as with outer CLR - //Swap -> "verificationMethod": "https://example.edu/issuers/565049#z6MkwA1498JfoCS3y4y3zggBDAosQEoCi5gsYH2PMXh1cFWK", - //To be like -> "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", - //...but also work properly which old record seems not be doing... - - /* - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); - */ + accumulator.add(new ProofVerifierProbe().run(endorsement, ctx)); } if(broken(accumulator)) return abort(ctx, accumulator, probeCount); @@ -113,7 +95,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector { //check refresh service if we are not already refreshed (check just like in external CLR) probeCount++; if(resource.getContext().get(REFRESHED) != TRUE) { - Optional newID = checkRefreshService(crd, ctx); + Optional newID = checkRefreshService(endorsement, ctx); if(newID.isPresent()) { //TODO resource.type return this.run( @@ -122,11 +104,11 @@ public class EndorsementInspector extends VCInspector implements SubInspector { } } - //revocation, expiration and issuance (check just like in external CLR) + //revocation, expiration and issuance for(Probe probe : List.of(new RevocationListProbe(), new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { probeCount++; - accumulator.add(probe.run(crd, ctx)); + accumulator.add(probe.run(endorsement, ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 13e58ac..8f8d07a 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -141,7 +141,7 @@ public class OB30Inspector extends VCInspector { //embedded endorsements EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); - List endorsements = asNodeList(crd.getJson(), "$..endorsement", jsonPath); + List endorsements = asNodeList(crd.getJson(), "$..endorsement", jsonPath); for(JsonNode node : endorsements) { probeCount++; Credential endorsement = new Credential(resource, node); @@ -149,7 +149,7 @@ public class OB30Inspector extends VCInspector { } //embedded jwt endorsements - endorsements = asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); + endorsements = asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); for(JsonNode node : endorsements) { probeCount++; String jwt = node.asText(); 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 index c0bb34c..676a0a6 100644 --- 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 @@ -51,7 +51,7 @@ public class ProofVerifierProbe extends Probe { } catch (DocumentError e) { return error(e.getType() + " " + e.getSubject(), ctx); } catch (VerificationError e) { - System.err.println(e.getCode()); + //System.err.println(e.getCode() + " (ProofVerifierProbe)"); if(e.getCode() == Code.Internal) { return exception(e.getMessage(), ctx.getResource()); } else if(e.getCode().equals(Code.Expired)) { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java index 3c17f25..804380e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java @@ -15,49 +15,50 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; /** - * A Probe that verifies a credential's type property. + * A Probe that verifies a credential's type property. + * * @author mgylling */ public class TypePropertyProbe extends Probe { private final Credential.Type expected; - + public TypePropertyProbe(Credential.Type expected) { super(ID); this.expected = checkNotNull(expected); } - + @Override public ReportItems run(JsonNode root, RunContext ctx) throws Exception { - ArrayNode typeNode = (ArrayNode)root.get("type"); - if(typeNode == null) return fatal("No type property", ctx); - + ArrayNode typeNode = (ArrayNode) root.get("type"); + if (typeNode == null) + return fatal("No type property", ctx); + List values = JsonNodeUtil.asStringList(typeNode); - - if(!values.contains("VerifiableCredential")) { + + if (!values.contains("VerifiableCredential")) { return fatal("The type property does not contain the entry 'VerifiableCredential'", ctx); } - - if(expected == Credential.Type.OpenBadgeCredential) { - if(!values.contains("OpenBadgeCredential") && !values.contains("AchievementCredential")) { - return fatal( - "The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", - ctx); - } - } - else if(expected == Credential.Type.ClrCredential){ - if(!values.contains("ClrCredential")) { - return fatal( - "The type property does not contain the entry 'ClrCredential'", - ctx); - } + + if (expected == Credential.Type.OpenBadgeCredential) { + if (!values.contains("OpenBadgeCredential") && !values.contains("AchievementCredential")) { + return fatal("The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", ctx); + } + } else if (expected == Credential.Type.ClrCredential) { + if (!values.contains("ClrCredential")) { + return fatal("The type property does not contain the entry 'ClrCredential'", ctx); + } + } else if (expected == Credential.Type.EndorsementCredential) { + if (!values.contains("EndorsementCredential")) { + return fatal("The type property does not contain the entry 'EndorsementCredential'", ctx); + } } else { - //TODO implement - throw new IllegalStateException(); + // TODO implement + throw new IllegalStateException(); } - + return success(ctx); } - - public static final String ID = TypePropertyProbe.class.getSimpleName(); + + public static final String ID = TypePropertyProbe.class.getSimpleName(); } From bbcfac14fcd5247c89a24abef819aca66ee1f5bf Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 20:30:10 +0200 Subject: [PATCH 39/58] add @context value check probe --- .../oneedtech/inspect/vc/OB30Inspector.java | 19 +++--- .../vc/probe/ContextPropertyProbe.java | 63 +++++++++++++++++++ .../org/oneedtech/inspect/vc/OB30Tests.java | 12 ++++ .../org/oneedtech/inspect/vc/Samples.java | 1 + .../resources/ob30/simple-err-context.json | 35 +++++++++++ 5 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java create mode 100644 inspector-vc/src/test/resources/ob30/simple-err-context.json diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 8f8d07a..02d532f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -30,6 +30,8 @@ import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.util.spec.Specification; +import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; @@ -89,16 +91,17 @@ public class OB30Inspector extends VCInspector { //we expect the above to place a generated object in the context Credential crd = ctx.getGeneratedObject(Credential.ID); - - //TODO check context IRIs? the schema doesnt do this - + //TODO new check: that subject @id or IdentityObject is available (at least one is the req) + + //context and type properties + Credential.Type type = Type.OpenBadgeCredential; + for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { + probeCount++; + accumulator.add(probe.run(crd.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } - //type property - probeCount++; - accumulator.add(new TypePropertyProbe(OpenBadgeCredential).run(crd.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - //canonical schema and inline schemata SchemaKey schema = crd.getSchemaKey().orElseThrow(); for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java new file mode 100644 index 0000000..53143af --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -0,0 +1,63 @@ +package org.oneedtech.inspect.vc.probe; + +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; + +import java.util.List; +import java.util.Map; + +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.vc.Credential; +import org.oneedtech.inspect.vc.util.JsonNodeUtil; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.common.collect.ImmutableMap; + +/** + * A Probe that verifies a credential's context property. + * + * @author mgylling + */ +public class ContextPropertyProbe extends Probe { + private final Credential.Type type; + + public ContextPropertyProbe(Credential.Type type) { + super(ID); + this.type = checkNotNull(type); + } + + @Override + public ReportItems run(JsonNode root, RunContext ctx) throws Exception { + + ArrayNode contextNode = (ArrayNode) root.get("@context"); + if (contextNode == null) { + return fatal("No @context property", ctx); + } + + List expected = values.get(type); + if(expected == null) { + return fatal(type.name() + " not recognized", ctx); + } + + List given = JsonNodeUtil.asStringList(contextNode); + int pos = 0; + for(String uri : expected) { + if((given.size() < pos+1) || !given.get(pos).equals(uri)) { + return error("missing required @context uri " + uri + " at position " + (pos+1), ctx); + } + pos++; + } + + return success(ctx); + } + + private final static Map> values = new ImmutableMap.Builder>() + .put(Credential.Type.OpenBadgeCredential, + List.of("https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json")) //TODO will change (https://purl.imsglobal.org/spec/ob/v3p0/context/ob_v3p0.jsonld) + .build(); + + public static final String ID = ContextPropertyProbe.class.getSimpleName(); +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 6b843e6..4471d18 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -10,6 +10,7 @@ import org.oneedtech.inspect.core.Inspector.Behavior; import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.test.PrintHelper; +import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; @@ -109,6 +110,17 @@ public class OB30Tests { }); } + @Test + void testSimpleJsonContextError() { + //removed one of the reqd context uris + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ERR_CONTEXT.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertHasProbeID(report, ContextPropertyProbe.ID, true); + }); + } + @Test void testSimpleJsonSchemaError() throws Exception { //issuer removed diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 0da143e..5ba1f27 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -17,6 +17,7 @@ public class Samples { public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-err-expired.json", false); public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false); public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false); + public final static Sample SIMPLE_JSON_ERR_CONTEXT = new Sample("ob30/simple-err-context.json", false); } public static final class PNG { public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true); diff --git a/inspector-vc/src/test/resources/ob30/simple-err-context.json b/inspector-vc/src/test/resources/ob30/simple-err-context.json new file mode 100644 index 0000000..5258f76 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-err-context.json @@ -0,0 +1,35 @@ +{ + "@context": [ + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file From e574c51fafade10abacee2fa6ba7d75afa815eea Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 21:42:00 +0200 Subject: [PATCH 40/58] use multikeymap --- .../vc/probe/ContextPropertyProbe.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java index 53143af..91fbb66 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -1,14 +1,18 @@ package org.oneedtech.inspect.vc.probe; +import static org.oneedtech.inspect.vc.Credential.Type.*; + import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; import java.util.List; import java.util.Map; +import java.util.Set; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; + import org.oneedtech.inspect.vc.util.JsonNodeUtil; import com.fasterxml.jackson.databind.JsonNode; @@ -33,31 +37,33 @@ public class ContextPropertyProbe extends Probe { ArrayNode contextNode = (ArrayNode) root.get("@context"); if (contextNode == null) { - return fatal("No @context property", ctx); - } - - List expected = values.get(type); - if(expected == null) { - return fatal(type.name() + " not recognized", ctx); + return fatal("No @context property", ctx); } + + List expected = values.get(values.keySet() + .stream() + .filter(s->s.contains(type)) + .findFirst() + .orElseThrow(()-> new IllegalArgumentException(type.name() + " not recognized"))); List given = JsonNodeUtil.asStringList(contextNode); int pos = 0; - for(String uri : expected) { - if((given.size() < pos+1) || !given.get(pos).equals(uri)) { - return error("missing required @context uri " + uri + " at position " + (pos+1), ctx); + for (String uri : expected) { + if ((given.size() < pos + 1) || !given.get(pos).equals(uri)) { + return error("missing required @context uri " + uri + " at position " + (pos + 1), ctx); } pos++; } - + return success(ctx); } - private final static Map> values = new ImmutableMap.Builder>() - .put(Credential.Type.OpenBadgeCredential, - List.of("https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json")) //TODO will change (https://purl.imsglobal.org/spec/ob/v3p0/context/ob_v3p0.jsonld) - .build(); - + private final static Map, List> values = new ImmutableMap.Builder, List>() + .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential), + List.of("https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json")) // TODO will change + // (https://purl.imsglobal.org/spec/ob/v3p0/context/ob_v3p0.jsonld) + .build(); + public static final String ID = ContextPropertyProbe.class.getSimpleName(); } From ccdcd6b0a53465942af16a8950c890d0f103244d Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 22:44:06 +0200 Subject: [PATCH 41/58] add context probe --- .../inspect/vc/EndorsementInspector.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 index 9801fc7..0b45345 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -24,6 +24,7 @@ import org.oneedtech.inspect.util.resource.Resource; import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; @@ -71,12 +72,15 @@ public class EndorsementInspector extends VCInspector implements SubInspector { List accumulator = new ArrayList<>(); int probeCount = 0; try { - - //type property - probeCount++; - accumulator.add(new TypePropertyProbe(Type.EndorsementCredential).run(endorsement.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - + + //context and type properties + Credential.Type type = Type.EndorsementCredential; + for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { + probeCount++; + accumulator.add(probe.run(endorsement.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + //inline schema (parent inspector has already validated against canonical) accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx)); From e1d4f76046488cb3a3b131c777e1e61de6d29ac3 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 22:44:22 +0200 Subject: [PATCH 42/58] add credential subject probe --- .../oneedtech/inspect/vc/OB30Inspector.java | 7 ++-- .../vc/probe/CredentialSubjectProbe.java | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 02d532f..c49bd80 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -31,6 +31,7 @@ import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.util.spec.Specification; import org.oneedtech.inspect.vc.Credential.Type; +import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; @@ -91,9 +92,7 @@ public class OB30Inspector extends VCInspector { //we expect the above to place a generated object in the context Credential crd = ctx.getGeneratedObject(Credential.ID); - - //TODO new check: that subject @id or IdentityObject is available (at least one is the req) - + //context and type properties Credential.Type type = Type.OpenBadgeCredential; for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { @@ -110,6 +109,8 @@ public class OB30Inspector extends VCInspector { if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } + accumulator.add(new CredentialSubjectProbe().run(crd.getJson(), ctx)); + //signatures, proofs probeCount++; if(crd.getJwt().isPresent()){ diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java new file mode 100644 index 0000000..2932563 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialSubjectProbe.java @@ -0,0 +1,42 @@ +package org.oneedtech.inspect.vc.probe; + +import org.oneedtech.inspect.core.probe.Probe; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * A Probe that checks credential subject specifics not capturable by schemata. + * + * @author mgylling + */ +public class CredentialSubjectProbe extends Probe { + + public CredentialSubjectProbe() { + super(ID); + } + + @Override + public ReportItems run(JsonNode root, RunContext ctx) throws Exception { + + JsonNode subject = root.get("credentialSubject"); + if(subject == null) return notRun("no credentialSubject node found", ctx); //error reported by schema + + /* + * Check that we have either .id or .identifier populated + */ + JsonNode id = root.get("id"); + if (id != null && id.textValue().strip().length() > 0) return success(ctx); + + JsonNode identifier = root.get("identifier"); + if(identifier != null && identifier instanceof ArrayNode + && ((ArrayNode)identifier).size() > 0) return success(ctx); + + return error("no id in credentialSubject", ctx); + + } + + public static final String ID = CredentialSubjectProbe.class.getSimpleName(); +} From 41f06f85e0c83c00ac460e87879c15b3c0f1f8fa Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Wed, 31 Aug 2022 22:53:38 +0200 Subject: [PATCH 43/58] Update OB30Inspector.java --- .../src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java | 1 + 1 file changed, 1 insertion(+) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index c49bd80..7ccae9b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -109,6 +109,7 @@ public class OB30Inspector extends VCInspector { if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } + //credentialSubject accumulator.add(new CredentialSubjectProbe().run(crd.getJson(), ctx)); //signatures, proofs From 23a34bd47073cf04bfdd443e50b4945f3923a4a0 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 1 Sep 2022 11:05:44 +0200 Subject: [PATCH 44/58] cleanup clr step #1 --- .../org/oneedtech/inspect/vc/Credential.java | 10 + .../inspect/vc/EmbeddedVCInspector.java | 175 ------------------ .../inspect/vc/EndorsementInspector.java | 22 +-- .../oneedtech/inspect/vc/OB30Inspector.java | 62 ++----- .../org/oneedtech/inspect/vc/VCInspector.java | 28 +++ .../vc/probe/ContextPropertyProbe.java | 10 +- .../org/oneedtech/inspect/vc/OB30Tests.java | 3 +- 7 files changed, 68 insertions(+), 242 deletions(-) delete mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java 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 index e88ab1b..d18a983 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -60,6 +60,11 @@ public class Credential extends GeneratedObject { return Optional.ofNullable(jwt); } + public ProofType getProofType() { + if(jwt == null) return ProofType.EMBEDDED; + return ProofType.EXTERNAL; + } + /** * Get the canonical schema for this credential if such exists. */ @@ -105,6 +110,11 @@ public class Credential extends GeneratedObject { } } + public enum ProofType { + EXTERNAL, + EMBEDDED + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java deleted file mode 100644 index 4a5a16f..0000000 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EmbeddedVCInspector.java +++ /dev/null @@ -1,175 +0,0 @@ -package org.oneedtech.inspect.vc; - -import static java.lang.Boolean.TRUE; - -import static org.oneedtech.inspect.core.probe.RunContext.Key.*; -import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; -import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; - -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import org.oneedtech.inspect.core.SubInspector; -import org.oneedtech.inspect.core.probe.GeneratedObject; -import org.oneedtech.inspect.core.probe.Probe; -import org.oneedtech.inspect.core.probe.RunContext; -import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator; -import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; -import org.oneedtech.inspect.core.report.Report; -import org.oneedtech.inspect.core.report.ReportItems; -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.UriResource; -import org.oneedtech.inspect.util.resource.context.ResourceContext; -import org.oneedtech.inspect.vc.Credential.Type; -import org.oneedtech.inspect.vc.probe.CredentialParseProbe; -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.RevocationListProbe; -import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; -import org.oneedtech.inspect.vc.probe.TypePropertyProbe; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * An inspector for EndorsementCredential objects. - * @author mgylling - */ -public class EmbeddedVCInspector extends VCInspector implements SubInspector { - - protected > EmbeddedVCInspector(B builder) { - super(builder); - } - - @Override - public Report run(Resource resource, Map parentObjects) { - /* - * The resource param is the top-level credential that embeds the endorsement, we - * expect parentObjects to provide a pointer to the JsonNode we should check - */ - Credential verifiableCredential = (Credential) parentObjects.get(VC_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(VC_KEY, verifiableCredential) - .build(); - - List accumulator = new ArrayList<>(); - int probeCount = 0; - - try { - //detect type (png, svg, json, jwt) and extract json data - probeCount++; - accumulator.add(new CredentialParseProbe().run(resource, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - - //we expect the above to place a generated object in the context - Credential crd = ctx.getGeneratedObject(Credential.ID); - - //type property - probeCount++; - accumulator.add(new TypePropertyProbe(Type.ClrCredential).run(crd.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - - //canonical schema and inline schema - SchemaKey schema = crd.getSchemaKey().orElseThrow(); - for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { - probeCount++; - accumulator.add(probe.run(crd.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - - //signatures, proofs - probeCount++; - if(crd.getJwt().isPresent()){ - //The credential originally contained in a JWT, validate the jwt and external proof. - accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); - } else { - //The credential not contained in a jwt, must have an internal proof. - //TODO: @Miles Need to fix the issuer, Same as with outer CLR - //Swap -> "verificationMethod": "https://example.edu/issuers/565049#z6MkwA1498JfoCS3y4y3zggBDAosQEoCi5gsYH2PMXh1cFWK", - //To be like -> "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", - //...but also work properly which old record seems not be doing... - - /* - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); - */ - - } - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - - //check refresh service if we are not already refreshed (check just like in external CLR) - probeCount++; - if(resource.getContext().get(REFRESHED) != TRUE) { - Optional newID = checkRefreshService(crd, ctx); - if(newID.isPresent()) { - //TODO resource.type - return this.run( - new UriResource(new URI(newID.get())) - .setContext(new ResourceContext(REFRESHED, TRUE))); - } - } - - //revocation, expiration and issuance (check just like in external CLR) - for(Probe probe : List.of(new RevocationListProbe(), - new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { - probeCount++; - accumulator.add(probe.run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - - } catch (Exception e) { - accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); - } - - return new Report(ctx, new ReportItems(accumulator), probeCount); - } - - @Override - public Report run(R resource) { - throw new IllegalStateException("must use #run(resource, map)"); - } - - public static class Builder extends VCInspector.Builder { - @SuppressWarnings("unchecked") - @Override - public EmbeddedVCInspector build() { - return new EmbeddedVCInspector(this); - } - } - - public static final String VC_KEY = "VC_KEY"; - private static final String REFRESHED = "is.refreshed.credential"; - - /** - * 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) { - JsonNode refreshServiceNode = crd.getJson().get("refreshService"); - if(refreshServiceNode != null) { - JsonNode serviceTypeNode = refreshServiceNode.get("type"); - if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { - JsonNode serviceURINode = refreshServiceNode.get("id"); - if(serviceURINode != null) { - return Optional.of(serviceURINode.asText()); - } - } - } - return Optional.empty(); - } -} 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 index 0b45345..94b3a9b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -137,25 +137,5 @@ public class EndorsementInspector extends VCInspector implements SubInspector { } public static final String ENDORSEMENT_KEY = "ENDORSEMENT_KEY"; - private static final String REFRESHED = "is.refreshed.credential"; - - /** - * 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) { - JsonNode refreshServiceNode = crd.getJson().get("refreshService"); - if(refreshServiceNode != null) { - JsonNode serviceTypeNode = refreshServiceNode.get("type"); - if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { - JsonNode serviceURINode = refreshServiceNode.get("id"); - if(serviceURINode != null) { - return Optional.of(serviceURINode.asText()); - } - } - } - return Optional.empty(); - } + } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 7ccae9b..57e583e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -4,7 +4,7 @@ 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.Credential.Type.OpenBadgeCredential; +import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL; import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY; import static org.oneedtech.inspect.vc.payload.PayloadParser.fromJwt; import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList; @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.oneedtech.inspect.core.Inspector; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext.Key; @@ -31,9 +32,9 @@ import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.util.spec.Specification; import org.oneedtech.inspect.vc.Credential.Type; -import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; +import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; @@ -54,9 +55,9 @@ import com.google.common.collect.ImmutableList; public class OB30Inspector extends VCInspector { protected final List> userProbes; - protected OB30Inspector(OB30Inspector.Builder builder) { + protected OB30Inspector(OB30Inspector.Builder builder) { super(builder); - this.userProbes = ImmutableList.copyOf(builder.probes); + this.userProbes = ImmutableList.copyOf(builder.probes); } //https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit# @@ -70,7 +71,7 @@ public class OB30Inspector extends VCInspector { JsonSchemaCache.reset(); CachingDocumentLoader.reset(); } - + ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); @@ -91,44 +92,43 @@ public class OB30Inspector extends VCInspector { 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); + Credential ob = ctx.getGeneratedObject(Credential.ID); //context and type properties Credential.Type type = Type.OpenBadgeCredential; for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { probeCount++; - accumulator.add(probe.run(crd.getJson(), ctx)); + accumulator.add(probe.run(ob.getJson(), ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } //canonical schema and inline schemata - SchemaKey schema = crd.getSchemaKey().orElseThrow(); + SchemaKey schema = ob.getSchemaKey().orElseThrow(); for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { probeCount++; - accumulator.add(probe.run(crd.getJson(), ctx)); + accumulator.add(probe.run(ob.getJson(), ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } //credentialSubject - accumulator.add(new CredentialSubjectProbe().run(crd.getJson(), ctx)); + accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx)); //signatures, proofs probeCount++; - if(crd.getJwt().isPresent()){ + if(ob.getProofType() == EXTERNAL){ //The credential originally contained in a JWT, validate the jwt and external proof. - accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + accumulator.add(new SignatureVerifierProbe().run(ob, ctx)); } else { //The credential not contained in a jwt, must have an internal proof. - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); + accumulator.add(new ProofVerifierProbe().run(ob, ctx)); } if(broken(accumulator)) return abort(ctx, accumulator, probeCount); //check refresh service if we are not already refreshed probeCount++; if(resource.getContext().get(REFRESHED) != TRUE) { - Optional newID = checkRefreshService(crd, ctx); + Optional newID = checkRefreshService(ob, ctx); if(newID.isPresent()) { - //TODO resource.type return this.run( new UriResource(new URI(newID.get())) .setContext(new ResourceContext(REFRESHED, TRUE))); @@ -139,14 +139,14 @@ public class OB30Inspector extends VCInspector { for(Probe probe : List.of(new RevocationListProbe(), new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { probeCount++; - accumulator.add(probe.run(crd, ctx)); + accumulator.add(probe.run(ob, ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } //embedded endorsements EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); - List endorsements = asNodeList(crd.getJson(), "$..endorsement", jsonPath); + List endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath); for(JsonNode node : endorsements) { probeCount++; Credential endorsement = new Credential(resource, node); @@ -154,7 +154,7 @@ public class OB30Inspector extends VCInspector { } //embedded jwt endorsements - endorsements = asNodeList(crd.getJson(), "$..endorsementJwt", jsonPath); + endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath); for(JsonNode node : endorsements) { probeCount++; String jwt = node.asText(); @@ -166,7 +166,7 @@ public class OB30Inspector extends VCInspector { //finally, run any user-added probes for(Probe probe : userProbes) { probeCount++; - accumulator.add(probe.run(crd, ctx)); + accumulator.add(probe.run(ob, ctx)); } } catch (Exception e) { @@ -175,29 +175,7 @@ public class OB30Inspector extends VCInspector { 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) { - JsonNode refreshServiceNode = crd.getJson().get("refreshService"); - if(refreshServiceNode != null) { - JsonNode serviceTypeNode = refreshServiceNode.get("type"); - if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { - JsonNode serviceURINode = refreshServiceNode.get("id"); - if(serviceURINode != null) { - return Optional.of(serviceURINode.asText()); - } - } - } - return Optional.empty(); - } - - private static final String REFRESHED = "is.refreshed.credential"; - + public static class Builder extends VCInspector.Builder { @SuppressWarnings("unchecked") @Override 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 index a07157d..255c54b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java @@ -2,6 +2,7 @@ package org.oneedtech.inspect.vc; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.oneedtech.inspect.core.Inspector; import org.oneedtech.inspect.core.probe.Outcome; @@ -10,6 +11,8 @@ import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.ReportItems; +import com.fasterxml.jackson.databind.JsonNode; + /** * Abstract base for verifiable credentials inspectors/verifiers. * @author mgylling @@ -25,12 +28,37 @@ public abstract class VCInspector extends Inspector { } protected boolean broken(List accumulator) { + if(getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) { + return false; + } for(ReportItems items : accumulator) { if(items.contains(Outcome.FATAL, Outcome.EXCEPTION, Outcome.NOT_RUN)) return true; } return false; } + /** + * If the AchievementCredential or EndorsementCredential has a “refreshService” property and the type of the + * RefreshService object is “1EdTechCredentialRefresh”, you should fetch the refreshed credential from the URL + * provided, then start the verification process over using the response as input. If the request fails, + * the credential is invalid. + */ + protected Optional checkRefreshService(Credential crd, RunContext ctx) { + JsonNode refreshServiceNode = crd.getJson().get("refreshService"); + if(refreshServiceNode != null) { + JsonNode serviceTypeNode = refreshServiceNode.get("type"); + if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { + JsonNode serviceURINode = refreshServiceNode.get("id"); + if(serviceURINode != null) { + return Optional.of(serviceURINode.asText()); + } + } + } + return Optional.empty(); + } + + protected static final String REFRESHED = "is.refreshed.credential"; + public abstract static class Builder> extends Inspector.Builder { final List> probes; diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java index 91fbb66..a72f26b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -37,7 +37,7 @@ public class ContextPropertyProbe extends Probe { ArrayNode contextNode = (ArrayNode) root.get("@context"); if (contextNode == null) { - return fatal("No @context property", ctx); + return notRun("No @context property", ctx); } List expected = values.get(values.keySet() @@ -59,10 +59,14 @@ public class ContextPropertyProbe extends Probe { } private final static Map, List> values = new ImmutableMap.Builder, List>() + // TODO uris will change .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential), List.of("https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json")) // TODO will change - // (https://purl.imsglobal.org/spec/ob/v3p0/context/ob_v3p0.jsonld) + "https://imsglobal.github.io/openbadges-specification/context.json")) + .put(Set.of(ClrCredential), + List.of("https://www.w3.org/2018/credentials/v1", + "https://dc.imsglobal.org/draft/clr/v2p0/context", + "https://imsglobal.github.io/openbadges-specification/context.json")) .build(); public static final String ID = ContextPropertyProbe.class.getSimpleName(); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 4471d18..1d2218b 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -26,7 +26,8 @@ public class OB30Tests { @BeforeAll static void setup() { validator = new OB30Inspector.Builder() - .set(Behavior.TEST_INCLUDE_SUCCESS, true) + .set(Behavior.TEST_INCLUDE_SUCCESS, true) + .set(Behavior.VALIDATOR_FAIL_FAST, true) .build(); } From 9da0e784a6780aa70e72120a2648065bfd1c1b31 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 1 Sep 2022 13:44:47 +0200 Subject: [PATCH 45/58] clean up clr step #2 --- .../org/oneedtech/inspect/vc/Credential.java | 3 +- .../inspect/vc/EndorsementInspector.java | 24 +-- .../oneedtech/inspect/vc/OB30Inspector.java | 199 +++++++++++------- .../org/oneedtech/inspect/vc/VCInspector.java | 8 +- ...fierProbe.java => EmbeddedProofProbe.java} | 8 +- ...erifierProbe.java => ExpirationProbe.java} | 6 +- ...fierProbe.java => ExternalProofProbe.java} | 8 +- .../vc/probe/InlineJsonSchemaProbe.java | 5 +- ...eVerifierProbe.java => IssuanceProbe.java} | 6 +- .../org/oneedtech/inspect/vc/OB30Tests.java | 12 +- 10 files changed, 162 insertions(+), 117 deletions(-) rename inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/{ProofVerifierProbe.java => EmbeddedProofProbe.java} (91%) rename inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/{ExpirationVerifierProbe.java => ExpirationProbe.java} (86%) rename inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/{SignatureVerifierProbe.java => ExternalProofProbe.java} (95%) rename inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/{IssuanceVerifierProbe.java => IssuanceProbe.java} (86%) 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 index d18a983..737c25b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -126,6 +126,7 @@ public class Credential extends GeneratedObject { } public static final String ID = Credential.class.getCanonicalName(); - public static final List RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT); + public static final List RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT); + public static final String CREDENTIAL_KEY = "CREDENTIAL_KEY"; } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java index 94b3a9b..2eb643c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java @@ -5,6 +5,8 @@ import static org.oneedtech.inspect.core.probe.RunContext.Key.*; import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; +import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY; +import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL; import java.net.URI; import java.util.ArrayList; @@ -25,12 +27,12 @@ import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.vc.Credential.Type; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; -import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; +import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe; +import org.oneedtech.inspect.vc.probe.ExpirationProbe; +import org.oneedtech.inspect.vc.probe.ExternalProofProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; -import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; -import org.oneedtech.inspect.vc.probe.ProofVerifierProbe; +import org.oneedtech.inspect.vc.probe.IssuanceProbe; import org.oneedtech.inspect.vc.probe.RevocationListProbe; -import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; import com.fasterxml.jackson.databind.JsonNode; @@ -58,7 +60,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector { * */ - Credential endorsement = (Credential) checkNotNull(parentObjects.get(ENDORSEMENT_KEY)); + Credential endorsement = (Credential) checkNotNull(parentObjects.get(CREDENTIAL_KEY)); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); @@ -86,12 +88,12 @@ public class EndorsementInspector extends VCInspector implements SubInspector { //signatures, proofs probeCount++; - if(endorsement.getJwt().isPresent()){ + if(endorsement.getProofType() == EXTERNAL){ //The credential originally contained in a JWT, validate the jwt and external proof. - accumulator.add(new SignatureVerifierProbe().run(endorsement, ctx)); + accumulator.add(new ExternalProofProbe().run(endorsement, ctx)); } else { //The credential not contained in a jwt, must have an internal proof. - accumulator.add(new ProofVerifierProbe().run(endorsement, ctx)); + accumulator.add(new EmbeddedProofProbe().run(endorsement, ctx)); } if(broken(accumulator)) return abort(ctx, accumulator, probeCount); @@ -110,7 +112,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector { //revocation, expiration and issuance for(Probe probe : List.of(new RevocationListProbe(), - new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { + new ExpirationProbe(), new IssuanceProbe())) { probeCount++; accumulator.add(probe.run(endorsement, ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); @@ -135,7 +137,5 @@ public class EndorsementInspector extends VCInspector implements SubInspector { 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 index 57e583e..96e4236 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -3,9 +3,10 @@ package org.oneedtech.inspect.vc; import static java.lang.Boolean.TRUE; import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN; import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; +import static org.oneedtech.inspect.util.code.Defensives.*; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; +import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY; import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL; -import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY; import static org.oneedtech.inspect.vc.payload.PayloadParser.fromJwt; import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList; @@ -15,7 +16,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import org.oneedtech.inspect.core.Inspector; +import org.oneedtech.inspect.core.SubInspector; +import org.oneedtech.inspect.core.probe.GeneratedObject; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext.Key; @@ -25,6 +27,7 @@ import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.schema.JsonSchemaCache; import org.oneedtech.inspect.schema.SchemaKey; +import org.oneedtech.inspect.util.code.Defensives; import org.oneedtech.inspect.util.json.ObjectMapperCache; import org.oneedtech.inspect.util.resource.Resource; import org.oneedtech.inspect.util.resource.ResourceType; @@ -35,12 +38,12 @@ import org.oneedtech.inspect.vc.Credential.Type; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; -import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; +import org.oneedtech.inspect.vc.probe.ExpirationProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; -import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; -import org.oneedtech.inspect.vc.probe.ProofVerifierProbe; +import org.oneedtech.inspect.vc.probe.IssuanceProbe; +import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe; import org.oneedtech.inspect.vc.probe.RevocationListProbe; -import org.oneedtech.inspect.vc.probe.SignatureVerifierProbe; +import org.oneedtech.inspect.vc.probe.ExternalProofProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; @@ -52,7 +55,7 @@ import com.google.common.collect.ImmutableList; * A verifier for Open Badges 3.0. * @author mgylling */ -public class OB30Inspector extends VCInspector { +public class OB30Inspector extends VCInspector implements SubInspector { protected final List> userProbes; protected OB30Inspector(OB30Inspector.Builder builder) { @@ -63,6 +66,14 @@ public class OB30Inspector extends VCInspector { //https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit# //https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#verificaton-and-validation + /* + * This inspector supports both standalone openbadge verification, as well as verification of + * AchievementCredentials embedded in e.g. CLR. + * + * When verifying a standalone AchievementCredential, call the run(Resource) method. When verifying + * an embedded AchievementCredential, call the run(Resource, Map) method. + */ + @Override public Report run(Resource resource) { super.check(resource); //TODO because URIs, this should be a fetch and cache @@ -89,80 +100,16 @@ public class OB30Inspector extends VCInspector { //detect type (png, svg, json, jwt) and extract json data probeCount++; accumulator.add(new CredentialParseProbe().run(resource, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount); //we expect the above to place a generated object in the context Credential ob = ctx.getGeneratedObject(Credential.ID); - - //context and type properties - Credential.Type type = Type.OpenBadgeCredential; - for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { - probeCount++; - accumulator.add(probe.run(ob.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - //canonical schema and inline schemata - SchemaKey schema = ob.getSchemaKey().orElseThrow(); - for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { - probeCount++; - accumulator.add(probe.run(ob.getJson(), ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } + //call the subinspector method of this + Report subReport = this.run(resource, Map.of(Credential.CREDENTIAL_KEY, ob)); + probeCount += subReport.getSummary().getTotalRun(); + accumulator.add(subReport); - //credentialSubject - accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx)); - - //signatures, proofs - probeCount++; - if(ob.getProofType() == EXTERNAL){ - //The credential originally contained in a JWT, validate the jwt and external proof. - accumulator.add(new SignatureVerifierProbe().run(ob, ctx)); - } else { - //The credential not contained in a jwt, must have an internal proof. - accumulator.add(new ProofVerifierProbe().run(ob, ctx)); - } - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - - //check refresh service if we are not already refreshed - probeCount++; - if(resource.getContext().get(REFRESHED) != TRUE) { - Optional newID = checkRefreshService(ob, ctx); - if(newID.isPresent()) { - return this.run( - new UriResource(new URI(newID.get())) - .setContext(new ResourceContext(REFRESHED, TRUE))); - } - } - - //revocation, expiration and issuance - for(Probe probe : List.of(new RevocationListProbe(), - new ExpirationVerifierProbe(), new IssuanceVerifierProbe())) { - probeCount++; - accumulator.add(probe.run(ob, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); - } - - //embedded endorsements - EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); - - List endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath); - for(JsonNode node : endorsements) { - probeCount++; - Credential endorsement = new Credential(resource, node); - accumulator.add(endorsementInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); - } - - //embedded jwt endorsements - endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath); - for(JsonNode node : endorsements) { - probeCount++; - String jwt = node.asText(); - JsonNode vcNode = fromJwt(jwt, ctx); - Credential endorsement = new Credential(resource, vcNode, jwt); - accumulator.add(endorsementInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); - } - //finally, run any user-added probes for(Probe probe : userProbes) { probeCount++; @@ -175,6 +122,103 @@ public class OB30Inspector extends VCInspector { return new Report(ctx, new ReportItems(accumulator), probeCount); } + + @Override + public Report run(Resource resource, Map parentObjects) { + + Credential ob = checkNotNull((Credential)parentObjects.get(CREDENTIAL_KEY)); + + ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); + JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); + RunContext ctx = new RunContext.Builder() + .put(this) + .put(resource) + .put(Key.JACKSON_OBJECTMAPPER, mapper) + .put(Key.JSONPATH_EVALUATOR, jsonPath) + .build(); + + List accumulator = new ArrayList<>(); + int probeCount = 0; + + try { + + //context and type properties + Credential.Type type = Type.OpenBadgeCredential; + for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { + probeCount++; + accumulator.add(probe.run(ob.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //canonical schema and inline schemata + SchemaKey schema = ob.getSchemaKey().orElseThrow(); + for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { + probeCount++; + accumulator.add(probe.run(ob.getJson(), ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //credentialSubject + probeCount++; + accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx)); + + //signatures, proofs + probeCount++; + if(ob.getProofType() == EXTERNAL){ + //The credential originally contained in a JWT, validate the jwt and external proof. + accumulator.add(new ExternalProofProbe().run(ob, ctx)); + } else { + //The credential not contained in a jwt, must have an internal proof. + accumulator.add(new EmbeddedProofProbe().run(ob, ctx)); + } + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + + //check refresh service if we are not already refreshed + probeCount++; + if(resource.getContext().get(REFRESHED) != TRUE) { + Optional newID = checkRefreshService(ob, ctx); + if(newID.isPresent()) { + return this.run( + new UriResource(new URI(newID.get())) + .setContext(new ResourceContext(REFRESHED, TRUE))); + } + } + + //revocation, expiration and issuance + for(Probe probe : List.of(new RevocationListProbe(), + new ExpirationProbe(), new IssuanceProbe())) { + probeCount++; + accumulator.add(probe.run(ob, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } + + //embedded endorsements + EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); + + List endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath); + for(JsonNode node : endorsements) { + probeCount++; + Credential endorsement = new Credential(resource, node); + accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement))); + } + + //embedded jwt endorsements + endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath); + for(JsonNode node : endorsements) { + probeCount++; + String jwt = node.asText(); + JsonNode vcNode = fromJwt(jwt, ctx); + Credential endorsement = new Credential(resource, vcNode, jwt); + accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement))); + } + + } catch (Exception e) { + accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e)); + } + + return new Report(ctx, new ReportItems(accumulator), probeCount); + + } public static class Builder extends VCInspector.Builder { @SuppressWarnings("unchecked") @@ -184,6 +228,5 @@ public class OB30Inspector extends VCInspector { 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 index 255c54b..a4a90dc 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java @@ -28,11 +28,15 @@ public abstract class VCInspector extends Inspector { } protected boolean broken(List accumulator) { - if(getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) { + return broken(accumulator, false); + } + + protected boolean broken(List accumulator, boolean force) { + if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) { return false; } for(ReportItems items : accumulator) { - if(items.contains(Outcome.FATAL, Outcome.EXCEPTION, Outcome.NOT_RUN)) return true; + if(items.contains(Outcome.FATAL, Outcome.EXCEPTION)) return true; } return false; } 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/EmbeddedProofProbe.java similarity index 91% rename from inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java rename to inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index 676a0a6..b3f3140 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -18,10 +18,10 @@ import com.apicatalog.vc.processor.StatusVerifier; import jakarta.json.JsonObject; /** - * A Probe that verifies a credential's proof. + * A Probe that verifies a credential's embedded proof. * @author mgylling */ -public class ProofVerifierProbe extends Probe { +public class EmbeddedProofProbe extends Probe { /* * Note: using com.apicatalog Iron, we get a generic VC verifier that @@ -32,7 +32,7 @@ public class ProofVerifierProbe extends Probe { * (aka is not a jwt). */ - public ProofVerifierProbe() { + public EmbeddedProofProbe() { super(ID); } @@ -71,5 +71,5 @@ public class ProofVerifierProbe extends Probe { } } - public static final String ID = ProofVerifierProbe.class.getSimpleName(); + public static final String ID = EmbeddedProofProbe.class.getSimpleName(); } 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/ExpirationProbe.java similarity index 86% rename from inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationVerifierProbe.java rename to inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java index 3660651..fcebd0c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java @@ -13,9 +13,9 @@ import com.fasterxml.jackson.databind.JsonNode; * A Probe that verifies a credential's expiration status * @author mgylling */ -public class ExpirationVerifierProbe extends Probe { +public class ExpirationProbe extends Probe { - public ExpirationVerifierProbe() { + public ExpirationProbe() { super(ID); } @@ -39,5 +39,5 @@ public class ExpirationVerifierProbe extends Probe { return success(ctx); } - public static final String ID = ExpirationVerifierProbe.class.getSimpleName(); + public static final String ID = ExpirationProbe.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/ExternalProofProbe.java similarity index 95% rename from inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java rename to inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java index a1dea0e..b4bccb0 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java @@ -36,12 +36,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Splitter; /** - * A Probe that verifies credential signatures + * A Probe that verifies credential external proof (jwt) * @author mlyon */ -public class SignatureVerifierProbe extends Probe { +public class ExternalProofProbe extends Probe { - public SignatureVerifierProbe() { + public ExternalProofProbe() { super(ID); } @@ -146,6 +146,6 @@ public class SignatureVerifierProbe extends Probe { return responseString; } - public static final String ID = SignatureVerifierProbe.class.getSimpleName(); + public static final String ID = ExternalProofProbe.class.getSimpleName(); } \ No newline at end of file diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java index e1179b8..7f3f86e 100644 --- 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 @@ -38,10 +38,7 @@ public class InlineJsonSchemaProbe extends Probe { 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 - + //note - we don't get deep nested ones in e.g. EndorsementCredential JsonNode credentialSchemaNode = root.get("credentialSchema"); if(credentialSchemaNode == null) return success(ctx); 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/IssuanceProbe.java similarity index 86% rename from inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java rename to inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java index 3203a0e..04c5d6b 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java @@ -13,9 +13,9 @@ import com.fasterxml.jackson.databind.JsonNode; * A Probe that verifies a credential's issuance status * @author mgylling */ -public class IssuanceVerifierProbe extends Probe { +public class IssuanceProbe extends Probe { - public IssuanceVerifierProbe() { + public IssuanceProbe() { super(ID); } @@ -39,5 +39,5 @@ public class IssuanceVerifierProbe extends Probe { return success(ctx); } - public static final String ID = IssuanceVerifierProbe.class.getSimpleName(); + public static final String ID = IssuanceProbe.class.getSimpleName(); } diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 1d2218b..15acfe5 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -11,10 +11,10 @@ import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.test.PrintHelper; import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; -import org.oneedtech.inspect.vc.probe.ExpirationVerifierProbe; +import org.oneedtech.inspect.vc.probe.ExpirationProbe; import org.oneedtech.inspect.vc.probe.InlineJsonSchemaProbe; -import org.oneedtech.inspect.vc.probe.IssuanceVerifierProbe; -import org.oneedtech.inspect.vc.probe.ProofVerifierProbe; +import org.oneedtech.inspect.vc.probe.IssuanceProbe; +import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; import com.google.common.collect.Iterables; @@ -96,7 +96,7 @@ public class OB30Tests { if(verbose) PrintHelper.print(report, true); assertInvalid(report); assertErrorCount(report, 1); - assertHasProbeID(report, ProofVerifierProbe.ID, true); + assertHasProbeID(report, EmbeddedProofProbe.ID, true); }); } @@ -107,7 +107,7 @@ public class OB30Tests { Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_EXPIRED.asFileResource()); if(verbose) PrintHelper.print(report, true); assertInvalid(report); - assertHasProbeID(report, ExpirationVerifierProbe.ID, true); + assertHasProbeID(report, ExpirationProbe.ID, true); }); } @@ -142,7 +142,7 @@ public class OB30Tests { Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_ISSUED.asFileResource()); if(verbose) PrintHelper.print(report, true); assertInvalid(report); - assertHasProbeID(report, IssuanceVerifierProbe.ID, true); + assertHasProbeID(report, IssuanceProbe.ID, true); }); } From e413bc1e68bf76d7855a26d0931ccd67f7837207 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 1 Sep 2022 18:46:48 +0200 Subject: [PATCH 46/58] Clean up schema provisioning --- .../org/oneedtech/inspect/vc/Credential.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 index 737c25b..e168988 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -2,9 +2,11 @@ package org.oneedtech.inspect.vc; import static org.oneedtech.inspect.util.code.Defensives.*; import static org.oneedtech.inspect.util.resource.ResourceType.*; +import static org.oneedtech.inspect.vc.Credential.Type.*; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Optional; import org.oneedtech.inspect.core.probe.GeneratedObject; @@ -16,6 +18,7 @@ import org.oneedtech.inspect.util.resource.ResourceType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; /** * A wrapper object for a verifiable credential. This contains e.g. the origin resource @@ -65,20 +68,19 @@ public class Credential extends GeneratedObject { return ProofType.EXTERNAL; } + + private static final Map schemas = new ImmutableMap.Builder() + .put(AchievementCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON) + .put(ClrCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON) + .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON) + .put(VerifiablePresentation, Catalog.CLR_20_CLRCREDENTIAL_JSON) + .build(); + /** * 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); - } else if(credentialType == Credential.Type.ClrCredential) { - return Optional.of(Catalog.CLR_20_CLRCREDENTIAL_JSON); - } - return Optional.empty(); + return Optional.ofNullable(schemas.get(credentialType)); } public enum Type { From 3c0a09624ec507f0b9a25d87bbed6a583034028d Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 1 Sep 2022 18:48:17 +0200 Subject: [PATCH 47/58] clean up prooftype getter --- .../src/main/java/org/oneedtech/inspect/vc/Credential.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index e168988..c19f703 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -64,8 +64,7 @@ public class Credential extends GeneratedObject { } public ProofType getProofType() { - if(jwt == null) return ProofType.EMBEDDED; - return ProofType.EXTERNAL; + return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL; } From 341a15b90a0b1786c444cc7c3c6eb35e571203df Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 9 Sep 2022 09:13:13 +0200 Subject: [PATCH 48/58] add trial impl of embedded proof verification using verifiable-credentials-java --- inspector-vc/pom.xml | 51 +- .../inspect/vc/probe/EmbeddedProofProbe.java | 103 ++- .../vc/util/CachingDocumentLoader.java | 53 +- .../resources/contexts/security-bbs-v1.jsonld | 92 +++ .../resources/contexts/security-v1.jsonld | 50 ++ .../resources/contexts/security-v2.jsonld | 59 ++ .../contexts/security-v3-unstable.jsonld | 710 ++++++++++++++++++ .../contexts/suites-ed25519-2018.jsonld | 91 +++ .../contexts/suites-ed25519-2020.jsonld | 93 +++ .../resources/contexts/suites-jws-2020.jsonld | 82 ++ .../contexts/suites-secp256k1-2019.jsonld | 102 +++ .../contexts/suites-x25519-2019.jsonld | 26 + .../org/oneedtech/inspect/vc/OB30Tests.java | 2 +- 13 files changed, 1432 insertions(+), 82 deletions(-) create mode 100644 inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/security-v1.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/security-v2.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld create mode 100644 inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index d0c5c65..841ab84 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -12,7 +12,7 @@ org.1edtech inspector-core - + com.auth0 auth0 @@ -28,14 +28,30 @@ java-jwt 3.19.2 + + + + com.danubetech + verifiable-credentials-java + + 1.1-SNAPSHOT + + + + + com.danubetech + key-formats-java + 2.1-SNAPSHOT + + com.apicatalog iron-verifiable-credentials-jre8 0.7.0 - - + + com.apicatalog titanium-json-ld @@ -48,27 +64,22 @@ 4.5.13 - + org.glassfish jakarta.json 2.0.1 + + + + danubetech-maven-public + https://repo.danubetech.com/repository/maven-public/ + + \ No newline at end of file diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index b3f3140..0beff3c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -1,6 +1,7 @@ package org.oneedtech.inspect.vc.probe; import java.io.StringReader; +import java.net.URI; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; @@ -8,23 +9,50 @@ import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; -import com.apicatalog.jsonld.document.JsonDocument; import com.apicatalog.ld.DocumentError; -import com.apicatalog.ld.signature.VerificationError; -import com.apicatalog.ld.signature.VerificationError.Code; -import com.apicatalog.vc.Vc; import com.apicatalog.vc.processor.StatusVerifier; +import com.danubetech.verifiablecredentials.VerifiableCredential; -import jakarta.json.JsonObject; +import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; /** * A Probe that verifies a credential's embedded proof. * @author mgylling */ public class EmbeddedProofProbe extends Probe { + + public EmbeddedProofProbe() { + super(ID); + } + + + /* + * Using verifiable-credentials-java (https://github.com/danubetech/verifiable-credentials-java) + */ + @Override + public ReportItems run(Credential crd, RunContext ctx) throws Exception { + + VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); + vc.setDocumentLoader(new CachingDocumentLoader()); + + URI method = vc.getLdProof().getVerificationMethod(); + byte[] publicKey = method.toString().getBytes(); + + Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey); + + try { + verifier.verify(vc); + } catch (Exception e) { + return fatal("Embedded proof verification failed:" + e.getMessage(), ctx); + } + + return success(ctx); + } + + /* - * Note: using com.apicatalog Iron, we get a generic VC verifier that + * Note: if using com.apicatalog Iron, we get a generic VC verifier that * will test other stuff than the Proof. So sometimes it may be that * Iron internally retests something that we're already testing out in the * Inspector class (e.g. expiration). But use this for now -- and remember @@ -32,39 +60,38 @@ public class EmbeddedProofProbe extends Probe { * (aka is not a jwt). */ - public EmbeddedProofProbe() { - super(ID); - } - - @Override - public ReportItems run(Credential crd, RunContext ctx) throws Exception { - JsonDocument jsonDoc = JsonDocument.of(new StringReader(crd.getJson().toString())); - JsonObject json = jsonDoc.getJsonContent().get().asJsonObject(); - try { - Vc.verify(json) - .loader(new CachingDocumentLoader()) - .useBundledContexts(false) //we control the cache in the loader - .statusVerifier(new NoopStatusVerifier()) - //.domain(...) - //.didResolver(...) - .isValid(); - } catch (DocumentError e) { - return error(e.getType() + " " + e.getSubject(), ctx); - } catch (VerificationError e) { - //System.err.println(e.getCode() + " (ProofVerifierProbe)"); - if(e.getCode() == Code.Internal) { - return exception(e.getMessage(), ctx.getResource()); - } else if(e.getCode().equals(Code.Expired)) { - //handled by other probe - } else { - return fatal(e.getCode().name() + " " + e.getMessage(), ctx); - } - - } - return success(ctx); - } +// /* +// * Using iron-verifiable-credentials (https://github.com/filip26/iron-verifiable-credentials) +// */ +// @Override +// public ReportItems run(Credential crd, RunContext ctx) throws Exception { +// JsonDocument jsonDoc = JsonDocument.of(new StringReader(crd.getJson().toString())); +// JsonObject json = jsonDoc.getJsonContent().get().asJsonObject(); +// try { +// Vc.verify(json) +// .loader(new CachingDocumentLoader()) +// .useBundledContexts(false) //we control the cache in the loader +// .statusVerifier(new IronNoopStatusVerifier()) +// //.domain(...) +// //.didResolver(...) +// .isValid(); +// } catch (DocumentError e) { +// return error(e.getType() + " " + e.getSubject(), ctx); +// } catch (VerificationError e) { +// //System.err.println(e.getCode() + " (ProofVerifierProbe)"); +// if(e.getCode() == Code.Internal) { +// return exception(e.getMessage(), ctx.getResource()); +// } else if(e.getCode().equals(Code.Expired)) { +// //handled by other probe +// } else { +// return fatal(e.getCode().name() + " " + e.getMessage(), ctx); +// } +// +// } +// return success(ctx); +// } - private static final class NoopStatusVerifier implements StatusVerifier { + private static final class IronNoopStatusVerifier implements StatusVerifier { @Override public void verify(Status status) throws DocumentError, VerifyError { // noop diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 2cefa2e..3c97a09 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -22,47 +22,54 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; /** - * A com.apicatalog DocumentLoader with a threadsafe static cache. + * A com.apicatalog DocumentLoader with a threadsafe static cache. + * * @author mgylling */ public class CachingDocumentLoader implements DocumentLoader { - + @Override - public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { + public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { Tuple tpl = new Tuple<>(url.toASCIIString(), options); try { - return documentCache.get(tpl); + return documentCache.get(tpl); } catch (Exception e) { logger.error("documentCache not able to load {}", url); throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage()); - } + } } - - static final ImmutableMap bundled = ImmutableMap.builder() - .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) - .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) - .put("https://w3id.org/security/suites/ed25519-2020/v1", Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) - .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) - .put("https://imsglobal.github.io/openbadges-specification/context.json", Resources.getResource("contexts/obv3.jsonld")) - .build(); - + + static final ImmutableMap bundled = ImmutableMap.builder() + .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) + .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) + .put("https://w3id.org/security/suites/ed25519-2020/v1",Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) + .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) + .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3.jsonld")) + .put("https://w3id.org/security/v1", Resources.getResource("contexts/security-v1.jsonld")) + .put("https://w3id.org/security/v2", Resources.getResource("contexts/security-v2.jsonld")) + .put("https://w3id.org/security/v3", Resources.getResource("contexts/security-v3-unstable.jsonld")) + .put("https://w3id.org/security/bbs/v1", Resources.getResource("contexts/security-bbs-v1.jsonld")) + .put("https://w3id.org/security/suites/secp256k1-2019/v1", Resources.getResource("contexts/suites-secp256k1-2019.jsonld")) + .put("https://w3id.org/security/suites/ed25519-2018/v1", Resources.getResource("contexts/suites-ed25519-2018.jsonld")) + .put("https://w3id.org/security/suites/x25519-2019/v1", Resources.getResource("contexts/suites-x25519-2019.jsonld")) + .put("https://w3id.org/security/suites/jws-2020/v1", Resources.getResource("contexts/suites-jws-2020.jsonld")) + + .build(); + static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder() - .initialCapacity(32) - .maximumSize(64) - .expireAfterAccess(Duration.ofHours(24)) + .initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24)) .build(new CacheLoader, Document>() { - public Document load(final Tuple id) throws Exception { - try (InputStream is = bundled.keySet().contains(id.t1) - ? bundled.get(id.t1).openStream() + public Document load(final Tuple id) throws Exception { + try (InputStream is = bundled.keySet().contains(id.t1) ? bundled.get(id.t1).openStream() : new URI(id.t1).toURL().openStream();) { - return JsonDocument.of(is); - } + return JsonDocument.of(is); + } } }); public static void reset() { documentCache.invalidateAll(); } - + private static final Logger logger = LogManager.getLogger(); } diff --git a/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld new file mode 100644 index 0000000..2bffafe --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/security-bbs-v1.jsonld @@ -0,0 +1,92 @@ +{ + "@context": { + "@version": 1.1, + "id": "@id", + "type": "@type", + "BbsBlsSignature2020": { + "@id": "https://w3id.org/security#BbsBlsSignature2020", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "proofValue": "https://w3id.org/security#proofValue", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "BbsBlsSignatureProof2020": { + "@id": "https://w3id.org/security#BbsBlsSignatureProof2020", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "Bls12381G2Key2020": "https://w3id.org/security#Bls12381G2Key2020" + } +} diff --git a/inspector-vc/src/main/resources/contexts/security-v1.jsonld b/inspector-vc/src/main/resources/contexts/security-v1.jsonld new file mode 100644 index 0000000..7529505 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/security-v1.jsonld @@ -0,0 +1,50 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + + "dc": "http://purl.org/dc/terms/", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + + "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016", + "Ed25519Signature2018": "sec:Ed25519Signature2018", + "EncryptedMessage": "sec:EncryptedMessage", + "GraphSignature2012": "sec:GraphSignature2012", + "LinkedDataSignature2015": "sec:LinkedDataSignature2015", + "LinkedDataSignature2016": "sec:LinkedDataSignature2016", + "CryptographicKey": "sec:Key", + + "authenticationTag": "sec:authenticationTag", + "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm", + "cipherAlgorithm": "sec:cipherAlgorithm", + "cipherData": "sec:cipherData", + "cipherKey": "sec:cipherKey", + "created": {"@id": "dc:created", "@type": "xsd:dateTime"}, + "creator": {"@id": "dc:creator", "@type": "@id"}, + "digestAlgorithm": "sec:digestAlgorithm", + "digestValue": "sec:digestValue", + "domain": "sec:domain", + "encryptionKey": "sec:encryptionKey", + "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"}, + "initializationVector": "sec:initializationVector", + "iterationCount": "sec:iterationCount", + "nonce": "sec:nonce", + "normalizationAlgorithm": "sec:normalizationAlgorithm", + "owner": {"@id": "sec:owner", "@type": "@id"}, + "password": "sec:password", + "privateKey": {"@id": "sec:privateKey", "@type": "@id"}, + "privateKeyPem": "sec:privateKeyPem", + "publicKey": {"@id": "sec:publicKey", "@type": "@id"}, + "publicKeyBase58": "sec:publicKeyBase58", + "publicKeyPem": "sec:publicKeyPem", + "publicKeyWif": "sec:publicKeyWif", + "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"}, + "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"}, + "salt": "sec:salt", + "signature": "sec:signature", + "signatureAlgorithm": "sec:signingAlgorithm", + "signatureValue": "sec:signatureValue" + } +} diff --git a/inspector-vc/src/main/resources/contexts/security-v2.jsonld b/inspector-vc/src/main/resources/contexts/security-v2.jsonld new file mode 100644 index 0000000..5f43a0c --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/security-v2.jsonld @@ -0,0 +1,59 @@ +{ + "@context": [{ + "@version": 1.1 + }, "https://w3id.org/security/v1", { + "AesKeyWrappingKey2019": "sec:AesKeyWrappingKey2019", + "DeleteKeyOperation": "sec:DeleteKeyOperation", + "DeriveSecretOperation": "sec:DeriveSecretOperation", + "EcdsaSecp256k1Signature2019": "sec:EcdsaSecp256k1Signature2019", + "EcdsaSecp256r1Signature2019": "sec:EcdsaSecp256r1Signature2019", + "EcdsaSecp256k1VerificationKey2019": "sec:EcdsaSecp256k1VerificationKey2019", + "EcdsaSecp256r1VerificationKey2019": "sec:EcdsaSecp256r1VerificationKey2019", + "Ed25519Signature2018": "sec:Ed25519Signature2018", + "Ed25519VerificationKey2018": "sec:Ed25519VerificationKey2018", + "EquihashProof2018": "sec:EquihashProof2018", + "ExportKeyOperation": "sec:ExportKeyOperation", + "GenerateKeyOperation": "sec:GenerateKeyOperation", + "KmsOperation": "sec:KmsOperation", + "RevokeKeyOperation": "sec:RevokeKeyOperation", + "RsaSignature2018": "sec:RsaSignature2018", + "RsaVerificationKey2018": "sec:RsaVerificationKey2018", + "Sha256HmacKey2019": "sec:Sha256HmacKey2019", + "SignOperation": "sec:SignOperation", + "UnwrapKeyOperation": "sec:UnwrapKeyOperation", + "VerifyOperation": "sec:VerifyOperation", + "WrapKeyOperation": "sec:WrapKeyOperation", + "X25519KeyAgreementKey2019": "sec:X25519KeyAgreementKey2019", + + "allowedAction": "sec:allowedAction", + "assertionMethod": {"@id": "sec:assertionMethod", "@type": "@id", "@container": "@set"}, + "authentication": {"@id": "sec:authenticationMethod", "@type": "@id", "@container": "@set"}, + "capability": {"@id": "sec:capability", "@type": "@id"}, + "capabilityAction": "sec:capabilityAction", + "capabilityChain": {"@id": "sec:capabilityChain", "@type": "@id", "@container": "@list"}, + "capabilityDelegation": {"@id": "sec:capabilityDelegationMethod", "@type": "@id", "@container": "@set"}, + "capabilityInvocation": {"@id": "sec:capabilityInvocationMethod", "@type": "@id", "@container": "@set"}, + "caveat": {"@id": "sec:caveat", "@type": "@id", "@container": "@set"}, + "challenge": "sec:challenge", + "ciphertext": "sec:ciphertext", + "controller": {"@id": "sec:controller", "@type": "@id"}, + "delegator": {"@id": "sec:delegator", "@type": "@id"}, + "equihashParameterK": {"@id": "sec:equihashParameterK", "@type": "xsd:integer"}, + "equihashParameterN": {"@id": "sec:equihashParameterN", "@type": "xsd:integer"}, + "invocationTarget": {"@id": "sec:invocationTarget", "@type": "@id"}, + "invoker": {"@id": "sec:invoker", "@type": "@id"}, + "jws": "sec:jws", + "keyAgreement": {"@id": "sec:keyAgreementMethod", "@type": "@id", "@container": "@set"}, + "kmsModule": {"@id": "sec:kmsModule"}, + "parentCapability": {"@id": "sec:parentCapability", "@type": "@id"}, + "plaintext": "sec:plaintext", + "proof": {"@id": "sec:proof", "@type": "@id", "@container": "@graph"}, + "proofPurpose": {"@id": "sec:proofPurpose", "@type": "@vocab"}, + "proofValue": "sec:proofValue", + "referenceId": "sec:referenceId", + "unwrappedKey": "sec:unwrappedKey", + "verificationMethod": {"@id": "sec:verificationMethod", "@type": "@id"}, + "verifyData": "sec:verifyData", + "wrappedKey": "sec:wrappedKey" + }] +} diff --git a/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld b/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld new file mode 100644 index 0000000..9c76d1a --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/security-v3-unstable.jsonld @@ -0,0 +1,710 @@ +{ + "@context": [{ + "@version": 1.1, + "id": "@id", + "type": "@type", + "@protected": true, + "JsonWebKey2020": { + "@id": "https://w3id.org/security#JsonWebKey2020" + }, + "JsonWebSignature2020": { + "@id": "https://w3id.org/security#JsonWebSignature2020", + "@context": { + "@version": 1.1, + "id": "@id", + "type": "@type", + "@protected": true, + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "Ed25519VerificationKey2020": { + "@id": "https://w3id.org/security#Ed25519VerificationKey2020" + }, + "Ed25519Signature2020": { + "@id": "https://w3id.org/security#Ed25519Signature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + }, + "ethereumAddress": { + "@id": "https://w3id.org/security#ethereumAddress" + }, + "publicKeyHex": { + "@id": "https://w3id.org/security#publicKeyHex" + }, + "blockchainAccountId": { + "@id": "https://w3id.org/security#blockchainAccountId" + }, + "MerkleProof2019": { + "@id": "https://w3id.org/security#MerkleProof2019" + }, + "Bls12381G1Key2020": { + "@id": "https://w3id.org/security#Bls12381G1Key2020" + }, + "Bls12381G2Key2020": { + "@id": "https://w3id.org/security#Bls12381G2Key2020" + }, + "BbsBlsSignature2020": { + "@id": "https://w3id.org/security#BbsBlsSignature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "BbsBlsSignatureProof2020": { + "@id": "https://w3id.org/security#BbsBlsSignatureProof2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + + "EcdsaKoblitzSignature2016": "https://w3id.org/security#EcdsaKoblitzSignature2016", + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "EncryptedMessage": "https://w3id.org/security#EncryptedMessage", + "GraphSignature2012": "https://w3id.org/security#GraphSignature2012", + "LinkedDataSignature2015": "https://w3id.org/security#LinkedDataSignature2015", + "LinkedDataSignature2016": "https://w3id.org/security#LinkedDataSignature2016", + "CryptographicKey": "https://w3id.org/security#Key", + "authenticationTag": "https://w3id.org/security#authenticationTag", + "canonicalizationAlgorithm": "https://w3id.org/security#canonicalizationAlgorithm", + "cipherAlgorithm": "https://w3id.org/security#cipherAlgorithm", + "cipherData": "https://w3id.org/security#cipherData", + "cipherKey": "https://w3id.org/security#cipherKey", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "creator": { + "@id": "http://purl.org/dc/terms/creator", + "@type": "@id" + }, + "digestAlgorithm": "https://w3id.org/security#digestAlgorithm", + "digestValue": "https://w3id.org/security#digestValue", + "domain": "https://w3id.org/security#domain", + "encryptionKey": "https://w3id.org/security#encryptionKey", + "expiration": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "initializationVector": "https://w3id.org/security#initializationVector", + "iterationCount": "https://w3id.org/security#iterationCount", + "nonce": "https://w3id.org/security#nonce", + "normalizationAlgorithm": "https://w3id.org/security#normalizationAlgorithm", + "owner": "https://w3id.org/security#owner", + "password": "https://w3id.org/security#password", + "privateKey": "https://w3id.org/security#privateKey", + "privateKeyPem": "https://w3id.org/security#privateKeyPem", + "publicKey": "https://w3id.org/security#publicKey", + "publicKeyBase58": "https://w3id.org/security#publicKeyBase58", + "publicKeyPem": "https://w3id.org/security#publicKeyPem", + "publicKeyWif": "https://w3id.org/security#publicKeyWif", + "publicKeyService": "https://w3id.org/security#publicKeyService", + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "salt": "https://w3id.org/security#salt", + "signature": "https://w3id.org/security#signature", + "signatureAlgorithm": "https://w3id.org/security#signingAlgorithm", + "signatureValue": "https://w3id.org/security#signatureValue", + "proofValue": "https://w3id.org/security#proofValue", + + "AesKeyWrappingKey2019": "https://w3id.org/security#AesKeyWrappingKey2019", + "DeleteKeyOperation": "https://w3id.org/security#DeleteKeyOperation", + "DeriveSecretOperation": "https://w3id.org/security#DeriveSecretOperation", + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "EcdsaSecp256k1VerificationKey2019": "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019", + "EcdsaSecp256r1VerificationKey2019": "https://w3id.org/security#EcdsaSecp256r1VerificationKey2019", + "Ed25519VerificationKey2018": "https://w3id.org/security#Ed25519VerificationKey2018", + "EquihashProof2018": "https://w3id.org/security#EquihashProof2018", + "ExportKeyOperation": "https://w3id.org/security#ExportKeyOperation", + "GenerateKeyOperation": "https://w3id.org/security#GenerateKeyOperation", + "KmsOperation": "https://w3id.org/security#KmsOperation", + "RevokeKeyOperation": "https://w3id.org/security#RevokeKeyOperation", + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@protected": true, + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "RsaVerificationKey2018": "https://w3id.org/security#RsaVerificationKey2018", + "Sha256HmacKey2019": "https://w3id.org/security#Sha256HmacKey2019", + "SignOperation": "https://w3id.org/security#SignOperation", + "UnwrapKeyOperation": "https://w3id.org/security#UnwrapKeyOperation", + "VerifyOperation": "https://w3id.org/security#VerifyOperation", + "WrapKeyOperation": "https://w3id.org/security#WrapKeyOperation", + "X25519KeyAgreementKey2019": "https://w3id.org/security#X25519KeyAgreementKey2019", + + "allowedAction": "https://w3id.org/security#allowedAction", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capability": { + "@id": "https://w3id.org/security#capability", + "@type": "@id" + }, + "capabilityAction": "https://w3id.org/security#capabilityAction", + "capabilityChain": { + "@id": "https://w3id.org/security#capabilityChain", + "@type": "@id", + "@container": "@list" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "caveat": { + "@id": "https://w3id.org/security#caveat", + "@type": "@id", + "@container": "@set" + }, + "challenge": "https://w3id.org/security#challenge", + "ciphertext": "https://w3id.org/security#ciphertext", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "delegator": { + "@id": "https://w3id.org/security#delegator", + "@type": "@id" + }, + "equihashParameterK": { + "@id": "https://w3id.org/security#equihashParameterK", + "@type": "http://www.w3.org/2001/XMLSchema#:integer" + }, + "equihashParameterN": { + "@id": "https://w3id.org/security#equihashParameterN", + "@type": "http://www.w3.org/2001/XMLSchema#:integer" + }, + "invocationTarget": { + "@id": "https://w3id.org/security#invocationTarget", + "@type": "@id" + }, + "invoker": { + "@id": "https://w3id.org/security#invoker", + "@type": "@id" + }, + "jws": "https://w3id.org/security#jws", + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "kmsModule": { + "@id": "https://w3id.org/security#kmsModule" + }, + "parentCapability": { + "@id": "https://w3id.org/security#parentCapability", + "@type": "@id" + }, + "plaintext": "https://w3id.org/security#plaintext", + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "referenceId": "https://w3id.org/security#referenceId", + "unwrappedKey": "https://w3id.org/security#unwrappedKey", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + }, + "verifyData": "https://w3id.org/security#verifyData", + "wrappedKey": "https://w3id.org/security#wrappedKey" + }] +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld b/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld new file mode 100644 index 0000000..6533c28 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/suites-ed25519-2018.jsonld @@ -0,0 +1,91 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "Ed25519VerificationKey2018": { + "@id": "https://w3id.org/security#Ed25519VerificationKey2018", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "publicKeyBase58": { + "@id": "https://w3id.org/security#publicKeyBase58" + } + } + }, + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "jws": { + "@id": "https://w3id.org/security#jws" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} diff --git a/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld b/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld new file mode 100644 index 0000000..b74da8c --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/suites-ed25519-2020.jsonld @@ -0,0 +1,93 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "Ed25519VerificationKey2020": { + "@id": "https://w3id.org/security#Ed25519VerificationKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "publicKeyMultibase": { + "@id": "https://w3id.org/security#publicKeyMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + }, + "Ed25519Signature2020": { + "@id": "https://w3id.org/security#Ed25519Signature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} diff --git a/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld b/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld new file mode 100644 index 0000000..17186cd --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/suites-jws-2020.jsonld @@ -0,0 +1,82 @@ +{ + "@context": { + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "JsonWebKey2020": { + "@id": "https://w3id.org/security#JsonWebKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + } + } + }, + "JsonWebSignature2020": { + "@id": "https://w3id.org/security#JsonWebSignature2020", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + + "id": "@id", + "type": "@type", + + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} diff --git a/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld b/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld new file mode 100644 index 0000000..5a345df --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/suites-secp256k1-2019.jsonld @@ -0,0 +1,102 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + }, + "EcdsaSecp256k1VerificationKey2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "blockchainAccountId": { + "@id": "https://w3id.org/security#blockchainAccountId" + }, + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + }, + "publicKeyBase58": { + "@id": "https://w3id.org/security#publicKeyBase58" + }, + "publicKeyMultibase": { + "@id": "https://w3id.org/security#publicKeyMultibase", + "@type": "https://w3id.org/security#multibase" + } + } + }, + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "jws": { + "@id": "https://w3id.org/security#jws" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} diff --git a/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld b/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld new file mode 100644 index 0000000..d01bac0 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/suites-x25519-2019.jsonld @@ -0,0 +1,26 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "@protected": true, + "X25519KeyAgreementKey2019": { + "@id": "https://w3id.org/security#X25519KeyAgreementKey2019", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "revoked": { + "@id": "https://w3id.org/security#revoked", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "publicKeyBase58": { + "@id": "https://w3id.org/security#publicKeyBase58" + } + } + } + } +} diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 15acfe5..4417dc2 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -21,7 +21,7 @@ import com.google.common.collect.Iterables; public class OB30Tests { private static OB30Inspector validator; - private static boolean verbose = false; + private static boolean verbose = true; @BeforeAll static void setup() { From 2ee1e92e6bb45fddf7cae7988c7aa5ba0ee5d4b0 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 9 Sep 2022 10:23:54 +0200 Subject: [PATCH 49/58] add new contexts to cache --- .../vc/util/CachingDocumentLoader.java | 9 +- .../src/main/resources/contexts/clr-v2p0.json | 53 +++ .../src/main/resources/contexts/ob-v3p0.json | 440 ++++++++++++++++++ .../contexts/{obv3.jsonld => obv3x.jsonld} | 0 4 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 inspector-vc/src/main/resources/contexts/clr-v2p0.json create mode 100644 inspector-vc/src/main/resources/contexts/ob-v3p0.json rename inspector-vc/src/main/resources/contexts/{obv3.jsonld => obv3x.jsonld} (100%) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 3c97a09..9f1afaf 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -40,11 +40,13 @@ public class CachingDocumentLoader implements DocumentLoader { } static final ImmutableMap bundled = ImmutableMap.builder() + .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("clr-v2p0.json")) + .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("ob-v3p0.json")) + .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3x.jsonld")) .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) .put("https://w3id.org/security/suites/ed25519-2020/v1",Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) - .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) - .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3.jsonld")) + .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) .put("https://w3id.org/security/v1", Resources.getResource("contexts/security-v1.jsonld")) .put("https://w3id.org/security/v2", Resources.getResource("contexts/security-v2.jsonld")) .put("https://w3id.org/security/v3", Resources.getResource("contexts/security-v3-unstable.jsonld")) @@ -60,7 +62,8 @@ public class CachingDocumentLoader implements DocumentLoader { .initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24)) .build(new CacheLoader, Document>() { public Document load(final Tuple id) throws Exception { - try (InputStream is = bundled.keySet().contains(id.t1) ? bundled.get(id.t1).openStream() + try (InputStream is = bundled.keySet().contains(id.t1) + ? bundled.get(id.t1).openStream() : new URI(id.t1).toURL().openStream();) { return JsonDocument.of(is); } diff --git a/inspector-vc/src/main/resources/contexts/clr-v2p0.json b/inspector-vc/src/main/resources/contexts/clr-v2p0.json new file mode 100644 index 0000000..d1e1b93 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/clr-v2p0.json @@ -0,0 +1,53 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + "ClrCredential": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#ClrCredential", + "@context": { + "id": "@id", + "type": "@type" + } + }, + "ClrSubject": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#ClrSubject", + "@context": { + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "obi": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#", + "achievement": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#achievement", + "@type": "obi:Achievement", + "@container": "@set" + }, + "association": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#association", + "@type": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#Association", + "@container": "@set" + }, + "verifiableCredential": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#verifiableCredential", + "@type": "cred:verifiableCredential", + "@container": "@set" + } + } + }, + "Association": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#Association", + "@context": { + "associationType": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#AssociationType" + }, + "sourceId": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#sourceId", + "@type": "xsd:anyURI" + }, + "targetId": { + "@id": "https://purl.imsglobal.org/spec/clr/v2p0/vocab.html#targetId", + "@type": "xsd:anyURI" + } + } + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/ob-v3p0.json b/inspector-vc/src/main/resources/contexts/ob-v3p0.json new file mode 100644 index 0000000..8e133c8 --- /dev/null +++ b/inspector-vc/src/main/resources/contexts/ob-v3p0.json @@ -0,0 +1,440 @@ +{ + "@context": { + "id": "@id", + "type": "@type", + + "xsd": "https://www.w3.org/2001/XMLSchema#", + + "OpenBadgeCredential": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#OpenBadgeCredential" + }, + "Achievement": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Achievement", + "@context": { + "achievementType": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#achievementType", + "@type": "xsd:string" + }, + "alignment": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#alignment", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Alignment", + "@container": "@set" + }, + "creator": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile" + }, + "creditsAvailable": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#creditsAvailable", + "@type": "xsd:float" + }, + "criteria": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Criteria", + "@type": "@id" + }, + "fieldOfStudy": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#fieldOfStudy", + "@type": "xsd:string" + }, + "humanCode": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#humanCode", + "@type": "xsd:string" + }, + "otherIdentifier": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#otherIdentifier", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry", + "@container": "@set" + }, + "related": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#related", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Related", + "@container": "@set" + }, + "resultDescription": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultDescription", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#ResultDescription", + "@container": "@set" + }, + "specialization": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#specialization", + "@type": "xsd:string" + }, + "tag": { + "@id": "https://schema.org/keywords", + "@type": "xsd:string", + "@container": "@set" + }, + "version": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#version", + "@type": "xsd:string" + } + } + }, + "AchievementCredential": { + "@id": "OpenBadgeCredential" + }, + "AchievementSubject": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#AchievementSubject", + "@context": { + "achievement": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Achievement" + }, + "activityEndDate": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#activityEndDate", + "@type": "xsd:date" + }, + "activityStartDate": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#activityStartDate", + "@type": "xsd:date" + }, + "creditsEarned": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#creditsEarned", + "@type": "xsd:float" + }, + "identifier": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifier", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentityObject", + "@container": "@set" + }, + "licenseNumber": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#licenseNumber", + "@type": "xsd:string" + }, + "result": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#result", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Result", + "@container": "@set" + }, + "role": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#role", + "@type": "xsd:string" + }, + "source": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#source", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile" + }, + "term": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#term", + "@type": "xsd:string" + } + } + }, + "Address": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Address", + "@context": { + "addressCountry": { + "@id": "https://schema.org/addressCountry", + "@type": "xsd:string" + }, + "addressCountryCode": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#CountryCode", + "@type": "xsd:string" + }, + "addressLocality": { + "@id": "https://schema.org/addressLocality", + "@type": "xsd:string" + }, + "addressRegion": { + "@id": "https://schema.org/addressRegion", + "@type": "xsd:string" + }, + "geo": { + "@id" : "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#GeoCoordinates" + }, + "postOfficeBoxNumber": { + "@id": "https://schema.org/postOfficeBoxNumber", + "@type": "xsd:string" + }, + "postalCode": { + "@id": "https://schema.org/postalCode", + "@type": "xsd:string" + }, + "streetAddress": { + "@id": "https://schema.org/streetAddress", + "@type": "xsd:string" + } + } + }, + "Alignment": { + "@id": "https://schema.org/Alignment", + "@context": { + "targetCode": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#targetCode", + "@type": "xsd:string" + }, + "targetDescription": { + "@id": "https://schema.org/targetDescription", + "@type": "xsd:string" + }, + "targetFramework": { + "@id": "https://schema.org/targetFramework", + "@type": "xsd:string" + }, + "targetName": { + "@id": "https://schema.org/targetName", + "@type": "xsd:string" + }, + "targetType": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#targetType", + "@type": "xsd:string" + }, + "targetUrl": { + "@id": "https://schema.org/targetUrl", + "@type": "xsd:anyURI" + } + } + }, + "Criteria": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Criteria" + }, + "EndorsementCredential": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementCredential" + }, + "EndorsementSubject": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementSubject", + "@context": { + "endorsementComment": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#endorsementComment", + "@type": "xsd:string" + } + } + }, + "Evidence": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Evidence", + "@context": { + "audience": { + "@id": "https://schema.org/audience", + "@type": "xsd:string" + }, + "genre": { + "@id": "https://schema.org/genre", + "@type": "xsd:string" + } + } + }, + "GeoCoordinates": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#GeoCoordinates", + "@context": { + "latitude": { + "@id": "https://schema.org/latitude", + "@type": "xsd:string" + }, + "longitude": { + "@id": "https://schema.org/longitude", + "@type": "xsd:string" + } + } + }, + "IdentifierEntry": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry", + "@context": { + "identifier": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifier", + "@type": "xsd:string" + }, + "identifierType": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identifierType", + "@type": "xsd:string" + } + } + }, + "IdentityObject": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentityObject", + "@context": { + "hashed": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#hashed", + "@type": "xsd:boolean" + }, + "identityHash": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identityHash", + "@type": "xsd:string" + }, + "identityType": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#identityType", + "@type": "xsd:string" + }, + "salt": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#salt", + "@type": "xsd:string" + } + } + }, + "Image": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Image", + "@context": { + "caption": { + "@id": "https://schema.org/caption", + "@type": "xsd:string" + } + } + }, + "Profile": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Profile", + "@context": { + "additionalName": { + "@id": "https://schema.org/additionalName", + "@type": "xsd:string" + }, + "address": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Address" + }, + "dateOfBirth": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#dateOfBirth", + "@type": "xsd:date" + }, + "email": { + "@id": "https://schema.org/email", + "@type": "xsd:string" + }, + "familyName": { + "@id": "https://schema.org/familyName", + "@type": "xsd:string" + }, + "familyNamePrefix": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#familyNamePrefix", + "@type": "xsd:string" + }, + "givenName": { + "@id": "https://schema.org/givenName", + "@type": "xsd:string" + }, + "honorificPrefix": { + "@id": "https://schema.org/honorificPrefix", + "@type": "xsd:string" + }, + "honorificSuffix": { + "@id": "https://schema.org/honorificSuffix", + "@type": "xsd:string" + }, + "otherIdentifier": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#otherIdentifier", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#IdentifierEntry", + "@container": "@set" + }, + "parentOrg": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#parentOrg", + "@type": "xsd:string" + }, + "patronymicName": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#patronymicName", + "@type": "xsd:string" + }, + "phone": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#PhoneNumber", + "@type": "xsd:string" + }, + "official": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#official", + "@type": "xsd:string" + } + } + }, + "Related": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Related", + "@context": { + "version": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#version", + "@type": "xsd:string" + } + } + }, + "Result": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Result", + "@context": { + "achievedLevel": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#achievedLevel", + "@type": "xsd:anyURI" + }, + "resultDescription": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultDescription", + "@type": "xsd:anyURI" + }, + "status": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#status", + "@type": "xsd:string" + }, + "value": { + "@id": "https://schema.org/value", + "@type": "xsd:string" + } + } + }, + "ResultDescription": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#ResultDescription", + "@context": { + "allowedValue": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#allowedValue", + "@type": "xsd:string", + "@container": "@set" + }, + "requiredLevel": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#requiredLevel", + "@type": "xsd:anyURI" + }, + "requiredValue": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#requiredValue", + "@type": "xsd:string" + }, + "resultType": { + "@id":"https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#resultType", + "@type": "xsd:string" + }, + "rubricCriterionLevel": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#rubricCriterionLevel", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#RubricCriterionLevel", + "@container": "@set" + }, + "valueMax": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#valueMax", + "@type": "xsd:string" + }, + "valueMin": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#valueMin", + "@type": "xsd:string" + } + } + }, + "RubricCriterionLevel": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#RubricCriterionLevel", + "@context": { + "level": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#level", + "@type": "xsd:string" + }, + "points": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#points", + "@type": "xsd:string" + } + } + }, + "alignment": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#alignment", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Alignment", + "@container": "@set" + }, + "description": { + "@id": "https://schema.org/description", + "@type": "xsd:string" + }, + "endorsement": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#endorsement", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#EndorsementCredential", + "@container": "@set" + }, + "image": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#image", + "@type": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#Image" + }, + "name": { + "@id": "https://schema.org/name", + "@type": "xsd:string" + }, + "narrative": { + "@id": "https://purl.imsglobal.org/spec/ob/v3p0/vocab.html#narrative", + "@type": "xsd:string" + }, + "url": { + "@id": "https://schema.org/url", + "@type": "xsd:anyURI" + } + } +} \ No newline at end of file diff --git a/inspector-vc/src/main/resources/contexts/obv3.jsonld b/inspector-vc/src/main/resources/contexts/obv3x.jsonld similarity index 100% rename from inspector-vc/src/main/resources/contexts/obv3.jsonld rename to inspector-vc/src/main/resources/contexts/obv3x.jsonld From 04cb5dd034e210988a093157479291403f29450b Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 9 Sep 2022 10:27:01 +0200 Subject: [PATCH 50/58] update contexts URLs --- .../inspect/vc/probe/ContextPropertyProbe.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java index a72f26b..5747336 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -58,15 +58,18 @@ public class ContextPropertyProbe extends Probe { return success(ctx); } - private final static Map, List> values = new ImmutableMap.Builder, List>() - // TODO uris will change + private final static Map, List> values = new ImmutableMap.Builder, List>() .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential), List.of("https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json")) + //"https://imsglobal.github.io/openbadges-specification/context.json")) //dev legacy + "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) .put(Set.of(ClrCredential), List.of("https://www.w3.org/2018/credentials/v1", - "https://dc.imsglobal.org/draft/clr/v2p0/context", - "https://imsglobal.github.io/openbadges-specification/context.json")) +// "https://dc.imsglobal.org/draft/clr/v2p0/context", //dev legacy +// "https://imsglobal.github.io/openbadges-specification/context.json")) //dev legacy + "https://purl.imsglobal.org/spec/clr/v2p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) + .build(); public static final String ID = ContextPropertyProbe.class.getSimpleName(); From f3d8b5ace6192bd8fb03fd504b3bba8706491b01 Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Fri, 9 Sep 2022 09:11:38 -0700 Subject: [PATCH 51/58] Update context URLs --- .../oneedtech/inspect/vc/probe/ContextPropertyProbe.java | 6 +++--- .../oneedtech/inspect/vc/util/CachingDocumentLoader.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java index a72f26b..6cc9dc1 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java @@ -62,11 +62,11 @@ public class ContextPropertyProbe extends Probe { // TODO uris will change .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential), List.of("https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json")) + "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) .put(Set.of(ClrCredential), List.of("https://www.w3.org/2018/credentials/v1", - "https://dc.imsglobal.org/draft/clr/v2p0/context", - "https://imsglobal.github.io/openbadges-specification/context.json")) + "https://purl.imsglobal.org/spec/clr/v2p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) .build(); public static final String ID = ContextPropertyProbe.class.getSimpleName(); diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 2cefa2e..91f2c54 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -43,7 +43,7 @@ public class CachingDocumentLoader implements DocumentLoader { .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) .put("https://w3id.org/security/suites/ed25519-2020/v1", Resources.getResource("contexts/security-suites-ed25519-2020-v1.jsonld")) .put("https://www.w3.org/2018/credentials/v1", Resources.getResource("contexts/2018-credentials-v1.jsonld")) - .put("https://imsglobal.github.io/openbadges-specification/context.json", Resources.getResource("contexts/obv3.jsonld")) + .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json", Resources.getResource("contexts/obv3.jsonld")) .build(); static final LoadingCache, Document> documentCache = CacheBuilder.newBuilder() From 5064f428eca481a173e1c34f9d5859a53c006264 Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Fri, 9 Sep 2022 09:58:55 -0700 Subject: [PATCH 52/58] Update CachingDocumentLoader.java Add 'contexts/' to path --- .../org/oneedtech/inspect/vc/util/CachingDocumentLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 9f1afaf..b656419 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -40,8 +40,8 @@ public class CachingDocumentLoader implements DocumentLoader { } static final ImmutableMap bundled = ImmutableMap.builder() - .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("clr-v2p0.json")) - .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("ob-v3p0.json")) + .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("contexts/clr-v2p0.json")) + .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("contexts/ob-v3p0.json")) .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3x.jsonld")) .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) From 1e63fdc9b36fe71d049b4b7c60d5b9813dd1b15a Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Fri, 9 Sep 2022 10:34:02 -0700 Subject: [PATCH 53/58] Fix key-formats-java version --- inspector-vc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 841ab84..8545db0 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -41,7 +41,7 @@ com.danubetech key-formats-java - 2.1-SNAPSHOT + 1.6-SNAPSHOT From b9eb3744d7385006f3d753d01dcd10f2019cf411 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Fri, 9 Sep 2022 19:54:17 +0200 Subject: [PATCH 54/58] Update CachingDocumentLoader.java --- .../org/oneedtech/inspect/vc/util/CachingDocumentLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java index 9f1afaf..b656419 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java @@ -40,8 +40,8 @@ public class CachingDocumentLoader implements DocumentLoader { } static final ImmutableMap bundled = ImmutableMap.builder() - .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("clr-v2p0.json")) - .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("ob-v3p0.json")) + .put("https://purl.imsglobal.org/spec/clr/v2p0/context.json",Resources.getResource("contexts/clr-v2p0.json")) + .put("https://purl.imsglobal.org/spec/ob/v3p0/context.json",Resources.getResource("contexts/ob-v3p0.json")) .put("https://imsglobal.github.io/openbadges-specification/context.json",Resources.getResource("contexts/obv3x.jsonld")) .put("https://www.w3.org/ns/did/v1", Resources.getResource("contexts/did-v1.jsonld")) .put("https://www.w3.org/ns/odrl.jsonld", Resources.getResource("contexts/odrl.jsonld")) From 07b3abcc5727e691913b263967e3729911a8224c Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:06:10 -0700 Subject: [PATCH 55/58] Update simple.json --- inspector-vc/src/test/resources/ob30/simple.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inspector-vc/src/test/resources/ob30/simple.json b/inspector-vc/src/test/resources/ob30/simple.json index 50e33aa..33c21a0 100644 --- a/inspector-vc/src/test/resources/ob30/simple.json +++ b/inspector-vc/src/test/resources/ob30/simple.json @@ -1,7 +1,7 @@ { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", "https://w3id.org/security/suites/ed25519-2020/v1" ], "id": "http://example.edu/credentials/3732", @@ -28,7 +28,7 @@ { "type": "Ed25519Signature2020", "created": "2022-06-28T16:28:36Z", - "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "verificationMethod": "https://example.edu/issuers/565049#z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", "proofPurpose": "assertionMethod", "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" } From f08e835e9535d176b6593048cc9a46326c9da82d Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Fri, 9 Sep 2022 12:07:22 -0700 Subject: [PATCH 56/58] Decode the public key before giving it to Ed25519Signature2020LdVerifier --- .../inspect/vc/probe/EmbeddedProofProbe.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index 0beff3c..dd27d79 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -3,6 +3,7 @@ package org.oneedtech.inspect.vc.probe; import java.io.StringReader; import java.net.URI; +import org.bouncycastle.util.Arrays; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; @@ -10,6 +11,7 @@ import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import com.apicatalog.ld.DocumentError; +import com.apicatalog.multibase.Multibase; import com.apicatalog.vc.processor.StatusVerifier; import com.danubetech.verifiablecredentials.VerifiableCredential; @@ -36,7 +38,42 @@ public class EmbeddedProofProbe extends Probe { vc.setDocumentLoader(new CachingDocumentLoader()); URI method = vc.getLdProof().getVerificationMethod(); - byte[] publicKey = method.toString().getBytes(); + + // The verification method must dereference to an Ed25519VerificationKey2020. + // Danubetech's Ed25519Signature2020LdVerifier expects the decoded public key + // from the Ed25519VerificationKey2020 (32 bytes). + + String publicKeyMultibase = ""; + + // Formats accepted: + // + // [controller]#[publicKeyMultibase] + // did:key:[publicKeyMultibase] + // [publicKeyMultibase] + + if (method.toString().contains("#")) { + publicKeyMultibase = method.getFragment(); + } else { + if (method.toString().startsWith("did")) { + String didScheme = method.getSchemeSpecificPart(); + if (didScheme.startsWith("key:")) { + publicKeyMultibase = didScheme.substring(4); + } else { + return fatal("Unknown verification method: " + method.toString(), ctx); + } + } else { + publicKeyMultibase = method.toString(); + } + } + + // Decode the Multibase to Multicodec and check that it is an Ed25519 public key + byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase); + if (publicKeyMulticodec[0] != -19 || publicKeyMulticodec[1] != 1) { + return fatal("Verification method does not contain an Ed25519 public key", ctx); + } + + // Extract the publicKey bytes from the Multicodec + byte[] publicKey = Arrays.copyOfRange(publicKeyMulticodec, 2, publicKeyMulticodec.length); Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey); From 4e35622d92f83df5f08ac2e78c128a76c320ff68 Mon Sep 17 00:00:00 2001 From: Markus Gylling Date: Thu, 15 Sep 2022 13:59:33 +0200 Subject: [PATCH 57/58] Update ob and clr schemas (renders from unifiedmodel) --- .../java/org/oneedtech/inspect/vc/Credential.java | 6 +++--- .../inspect/vc/probe/EmbeddedProofProbe.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) 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 index c19f703..1b3c501 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -70,9 +70,9 @@ public class Credential extends GeneratedObject { private static final Map schemas = new ImmutableMap.Builder() .put(AchievementCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON) - .put(ClrCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON) - .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON) + .put(ClrCredential, Catalog.CLR_20_CLRCREDENTIAL_JSON) .put(VerifiablePresentation, Catalog.CLR_20_CLRCREDENTIAL_JSON) + .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON) .build(); /** @@ -85,7 +85,7 @@ public class Credential extends GeneratedObject { public enum Type { AchievementCredential, OpenBadgeCredential, //treated as an alias of AchievementCredential - ClrCredential, //NOT a duplicate of OB this does not use an alias and we ONLY use 'ClrCredential' as the base type + ClrCredential, EndorsementCredential, VerifiablePresentation, VerifiableCredential, //this is an underspecifier in our context diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index dd27d79..cafea8a 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -34,6 +34,9 @@ public class EmbeddedProofProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { + //TODO check that proof is Ed25519 - issue error if not ("type": "Ed25519Signature2020", + //TODO check value "proofPurpose": "assertionMethod", if not error + VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); vc.setDocumentLoader(new CachingDocumentLoader()); @@ -51,6 +54,10 @@ public class EmbeddedProofProbe extends Probe { // did:key:[publicKeyMultibase] // [publicKeyMultibase] + // TODO fourth format that we don't support yet: a URL that returns a Ed25519VerificationKey2020 + // if starts with http and does not have hashcode, try fetch and see if returns Ed25519VerificationKey2020 + // property is publicKeyMultibase + if (method.toString().contains("#")) { publicKeyMultibase = method.getFragment(); } else { @@ -77,6 +84,11 @@ public class EmbeddedProofProbe extends Probe { Ed25519Signature2020LdVerifier verifier = new Ed25519Signature2020LdVerifier(publicKey); + //TODO find out whether we also should check that controller matches issuer ID: + // if [controller]#[publicKeyMultibase] format - check [controller] segment + // if did:key:[publicKeyMultibase] format: issuer ID must match the entire URI + // if [publicKeyMultibase] -- don't check issuer ID. Maybe we should warn about this syntax. + try { verifier.verify(vc); } catch (Exception e) { From 5941a9ab4ec3d31b836edd735661cdc00322d8ca Mon Sep 17 00:00:00 2001 From: "Andy Miller (IMS)" <48326098+amiller-ims@users.noreply.github.com> Date: Thu, 15 Sep 2022 13:06:35 -0700 Subject: [PATCH 58/58] Update ob30 samples --- .../inspect/vc/probe/EmbeddedProofProbe.java | 18 ++-- .../org/oneedtech/inspect/vc/OB30Tests.java | 16 +++- .../org/oneedtech/inspect/vc/Samples.java | 3 +- .../ob30/simple-err-proof-method.json | 54 ++++++++++++ .../ob30/simple-err-proof-value.json | 54 ++++++++++++ .../test/resources/ob30/simple-err-proof.json | 36 -------- .../src/test/resources/ob30/simple-json.png | Bin 83704 -> 84440 bytes .../src/test/resources/ob30/simple-json.svg | 82 +++++++++++------- .../src/test/resources/ob30/simple-jwt.png | Bin 84446 -> 85229 bytes .../src/test/resources/ob30/simple-jwt.svg | 2 +- .../src/test/resources/ob30/simple.json | 26 +++++- 11 files changed, 210 insertions(+), 81 deletions(-) create mode 100644 inspector-vc/src/test/resources/ob30/simple-err-proof-method.json create mode 100644 inspector-vc/src/test/resources/ob30/simple-err-proof-value.json delete mode 100644 inspector-vc/src/test/resources/ob30/simple-err-proof.json diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java index cafea8a..554a0cf 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java @@ -66,7 +66,7 @@ public class EmbeddedProofProbe extends Probe { if (didScheme.startsWith("key:")) { publicKeyMultibase = didScheme.substring(4); } else { - return fatal("Unknown verification method: " + method.toString(), ctx); + return error("Unknown verification method: " + method.toString(), ctx); } } else { publicKeyMultibase = method.toString(); @@ -74,9 +74,14 @@ public class EmbeddedProofProbe extends Probe { } // Decode the Multibase to Multicodec and check that it is an Ed25519 public key - byte[] publicKeyMulticodec = Multibase.decode(publicKeyMultibase); - if (publicKeyMulticodec[0] != -19 || publicKeyMulticodec[1] != 1) { - return fatal("Verification method does not contain an Ed25519 public key", ctx); + byte[] publicKeyMulticodec; + try { + publicKeyMulticodec = Multibase.decode(publicKeyMultibase); + if (publicKeyMulticodec[0] != -19 || publicKeyMulticodec[1] != 1) { + return error("Verification method does not contain an Ed25519 public key", ctx); + } + } catch (Exception e) { + return error("Verification method is invalid: " + e.getMessage(), ctx); } // Extract the publicKey bytes from the Multicodec @@ -90,7 +95,10 @@ public class EmbeddedProofProbe extends Probe { // if [publicKeyMultibase] -- don't check issuer ID. Maybe we should warn about this syntax. try { - verifier.verify(vc); + boolean verify = verifier.verify(vc); + if (!verify) { + return error("Embedded proof verification failed.", ctx); + } } catch (Exception e) { return fatal("Embedded proof verification failed:" + e.getMessage(), ctx); } diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 4417dc2..0be0215 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -89,10 +89,22 @@ public class OB30Tests { } @Test - void testSimpleJsonInvalidProof() { + void testSimpleJsonInvalidProofMethod() { //add some garbage chars to proofValue assertDoesNotThrow(()->{ - Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_ERROR.asFileResource()); + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_METHOD_ERROR.asFileResource()); + if(verbose) PrintHelper.print(report, true); + assertInvalid(report); + assertErrorCount(report, 1); + assertHasProbeID(report, EmbeddedProofProbe.ID, true); + }); + } + + @Test + void testSimpleJsonInvalidProofValue() { + //add some garbage chars to proofValue + assertDoesNotThrow(()->{ + Report report = validator.run(Samples.OB30.JSON.SIMPLE_JSON_PROOF_VALUE_ERROR.asFileResource()); if(verbose) PrintHelper.print(report, true); assertInvalid(report); assertErrorCount(report, 1); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java index 5ba1f27..3c10f32 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java @@ -13,7 +13,8 @@ public class Samples { public final static Sample SIMPLE_JSON = new Sample("ob30/simple.json", true); public final static Sample SIMPLE_JSON_NOPROOF = new Sample("ob30/simple-noproof.json", false); public final static Sample SIMPLE_JSON_UNKNOWN_TYPE = new Sample("ob30/simple-err-type.json", false); - public final static Sample SIMPLE_JSON_PROOF_ERROR = new Sample("ob30/simple-err-proof.json", false); + public final static Sample SIMPLE_JSON_PROOF_METHOD_ERROR = new Sample("ob30/simple-err-proof-method.json", false); + public final static Sample SIMPLE_JSON_PROOF_VALUE_ERROR = new Sample("ob30/simple-err-proof-value.json", false); public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-err-expired.json", false); public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false); public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false); diff --git a/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json b/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json new file mode 100644 index 0000000..037a10c --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-err-proof-method.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-09-15T15:48:32Z", + "verificationMethod": "https://example.edu/issuers/565049#xxMkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj", + "proofPurpose": "assertionMethod", + "proofValue": "z3yUuWbFsLUp2CUrSZRaRbTk1UnkhpoJgJYu1SdMqd3AEMotpY41sKky7VzavnSfjApggtWJg1tcREvs5H4ZNnBRH" + } + ] +} \ No newline at end of file diff --git a/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json b/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json new file mode 100644 index 0000000..d26f49d --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-err-proof-value.json @@ -0,0 +1,54 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-09-15T15:48:32Z", + "verificationMethod": "https://example.edu/issuers/565049#z6MkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj", + "proofPurpose": "assertionMethod", + "proofValue": "z3fQCWGpz7b1HSH6DTwYiH5vutqtpJb5SHiP1VFK22xeBEW2D61tC9j3SktwPLNxPnTNZnPt4GeAZJPdVYserRqs4" + } + ] +} \ No newline at end of file diff --git a/inspector-vc/src/test/resources/ob30/simple-err-proof.json b/inspector-vc/src/test/resources/ob30/simple-err-proof.json deleted file mode 100644 index f749e52..0000000 --- a/inspector-vc/src/test/resources/ob30/simple-err-proof.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "@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-28T16:28:36Z", - "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", - "proofPurpose": "assertionMethod", - "proofValue": "XXXz3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnMXXX" - } - ] -} \ 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 index f13d2447a0ec9fcc5314ac7d7bedcec723b3b560..ee80f0abe5cd18f2f1bd5ea03294aac9c481500d 100644 GIT binary patch delta 976 zcmah{O>fgM7$!*LFbTmGA%qDx#G0l1fE5n(BU>r!B+_FoaTcizkd8Y&-?iO`)TRd=cNzdj32e@H~wTlYX-T` zhaxVKLUuDGZ7bN@ihl3c%Ui zCBjl0aw*K(HYP zn5IN9n!haNX=?ph63IM+BIv^0ncfR`rY|tkPY8@Veu$Gi$656GM-~hO<(5OGxU5*# zRZ}7h7VB6}8P)T3l+V}E!WdlY9nGMnW2r-#Cw1}trtP3E_QRIGoEh7PF+or)98_cy@DZ#op-J8iN@e{Z{k2y`!&5@I(v<;wRa!?0mG*`IRF3v delta 282 zcmcaHne|66Yr_`Ct4>TTywk5cF=pyy<`$>tNO=%r_tWRxc9W#;SW7o_GTC8ngO z7V8!lq$X#kWhN( diff --git a/inspector-vc/src/test/resources/ob30/simple-jwt.png b/inspector-vc/src/test/resources/ob30/simple-jwt.png index 84a9ab2fa642d0a39d8779b00ce66e1bfbd1505f..7211f537e503ddc95bb54385ce149393f943284f 100644 GIT binary patch delta 1819 zcmY+DyN>&~6~;5W*d|4a^eOTH8|Y$tMnD?N7Ihh2#*{^hZ2l{ewk4aoj4fLi1aOd7 zNHNJvY_-Am`wl_6yg{nGK%kIEnq%3ip8yvz>r~D_xljqSBO(38y#)3B!ifZ%S=d#JXr9B_c~ zVaR~j1ZrU+V7?&${Y<4j)P%Res9{^~LdUz1YDlOxQ@4qDhq!$UYf#`VETjOJq-L;7 zse=-tW!hlp8tbApjqs_9k`yW&c{hqXk?YjtxESsf6aiNtqP2I8u%KJ3>BFkSn^X8I zFL!vXLV|>uC%|NubO<~k*9>uouB|=>=(M%Kn(DwwHE_cAsbdv) z65zRE)!NWD=w@QtZVjW&WZsZ%gt5*(eq}vncjllmU(MS*4eQ)CN`m`qu6wnw%gk|b z%Qg4Lk6|CIjAO3N6`B@`U$s-N%}RgeHQa$co0^-in<}gNYlhW{YQv3G;Nn-tNJnF0 zHahy6QTjTW*m`+;^q_b?W!U)o%Ell6WDO^DFAtjxBaLEJ zzSol2UF=5hN>XnmDQtO_lu_v=6obyS<;~a=tPT$HT)6i9mSZ9Rg@UfkD%afuw~LmI zrc?`S8k^-VHkHgqEh|kK85-qTkyo1otB{P0U+-DkK8jUl8&}uyqzmapHjss2XM>K} zgK(|z#_HuE$KXmZmm<)WuV&?5#zJj05#4K<1KUUL53w?h$+f+L+FU$(%9QH75(}CJ z4}ns++^Gs(a^1XtM&{Z^D3vL@@a9j>i>>25&J2A0Q)>VbV9dnAxmj}`^0rNuRxu8yR%hGV-pDetn--XAd z57(#1zFbK~%IIi~;nAIf^`;NjOBq+IWDg;q4VRBZ#Lry#(glx=|GAN7B>hu31>?g6 z(^2_h+fT;5R(^LL(QtcptPQwoxw5*d{$jQ?l;%*r&+NnL{WdQ(8a}s0(izK0c>DL) zjccDLFjy2-IXX=MCsC}ND-YE2qR51`ZfRekLKTBKm7g6hflw+{*=G|=F8{5DNTM*C!P>=dy zEBJ-EcS9_u0jc7WR6wPy7nx$F=d;a_8ZjnE3M^Vt+a*fA_X|xS%8pGcZw+V21e|7M z6`o_(tkd%Sfot56TEp?1Kh9hFoAuf@CN}(e( zol}Hkm-Ph1*b!l4Hmq!(C>n_P-EGn8kfXkb-p7QYS6jisV#Fj$$)vo^hf&G+0UEOK zR%a-0ZTN^26m*gza^)6hGm zoL#!ic1k1D3Lu&iq<;QZU7fU3$j+F13~{CPR<)c_&)N7Qw8JREv=z^C9Va$WTq;Ul}Txib%6e))Tb;2Q-fNx6QK|Ph6;xtN^!Iq)e zIYa%1-li~9)dSnZI}hX&6=O}xL zZ0tHibo002?2%&Tt0+Om7(?dvG+w7oP`^*$uwLPad?kDI8`u>znlym+$L-w@Ia_WnS2KZa)+24F&EtZeS}M)%qpXb{6jp#0{x% zrth-tfPQ+=yzMZ*k;~;Nf#$=siIpzMF5A86>MYBxjVqU@yITlQC(cQj=U0x`FAp&( zl`Ulh09@~=u@QSdc(f-E*Q0h=$nSxFYyo%otHu&uvsvk>wmxpJ!kekQY - + diff --git a/inspector-vc/src/test/resources/ob30/simple.json b/inspector-vc/src/test/resources/ob30/simple.json index 33c21a0..6c48673 100644 --- a/inspector-vc/src/test/resources/ob30/simple.json +++ b/inspector-vc/src/test/resources/ob30/simple.json @@ -2,6 +2,7 @@ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://purl.imsglobal.org/spec/ob/v3p0/context.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json", "https://w3id.org/security/suites/ed25519-2020/v1" ], "id": "http://example.edu/credentials/3732", @@ -22,15 +23,32 @@ "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", "type": [ "AchievementSubject" - ] + ], + "achievement": { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "type": [ + "Achievement" + ], + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "name": "Teamwork" + } }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], "proof": [ { "type": "Ed25519Signature2020", - "created": "2022-06-28T16:28:36Z", - "verificationMethod": "https://example.edu/issuers/565049#z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "created": "2022-09-15T15:48:32Z", + "verificationMethod": "https://example.edu/issuers/565049#z6MkmY1R6tG2NEdRHzphdRT6JqxeYpHwLAHwbrDfQULpkMAj", "proofPurpose": "assertionMethod", - "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + "proofValue": "z3yUuWbFsLUp2CUrSZRaRbTk1UnkhpoJgJYu1SdMqd3AEMotpY41sKky7VzavnSfjApggtWJg1tcREvs5H4ZNnBRH" } ] } \ No newline at end of file