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