From 6c9485cf029cb9b1fe2ce3d4c522d5a961b682c3 Mon Sep 17 00:00:00 2001 From: Xavi Aracil Date: Tue, 22 Nov 2022 18:04:02 +0100 Subject: [PATCH] parse now return the generic credential, with keys from context --- .../inspect/vc/payload/JsonParser.java | 14 ++-- .../inspect/vc/payload/JwtParser.java | 12 ++- .../inspect/vc/payload/PayloadParser.java | 18 +++-- .../inspect/vc/payload/PngParser.java | 62 +++++++++------ .../inspect/vc/payload/SvgParser.java | 61 ++++++++++----- .../vc/probe/CredentialParseProbe.java | 27 +++---- .../vc/credential/PayloadParserTests.java | 75 +++++++++---------- 7 files changed, 164 insertions(+), 105 deletions(-) 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 index 6552312..c500743 100644 --- 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 @@ -4,10 +4,10 @@ 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.core.probe.RunContext.Key; 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.AbstractBaseCredential; import com.fasterxml.jackson.databind.JsonNode; /** @@ -22,11 +22,15 @@ public final class JsonParser extends PayloadParser { } @Override - public Credential parse(Resource resource, RunContext ctx) throws Exception { + public AbstractBaseCredential 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); + JsonNode node = fromString(json, ctx); + + return getBuilder(ctx) + .resource(resource) + .jsonData(node) + .build(); } } 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 index 3a946b1..eece82b 100644 --- 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 @@ -6,7 +6,7 @@ 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 org.oneedtech.inspect.vc.AbstractBaseCredential; import com.fasterxml.jackson.databind.JsonNode; @@ -22,11 +22,15 @@ public final class JwtParser extends PayloadParser { } @Override - public Credential parse(Resource resource, RunContext ctx) throws Exception { + public AbstractBaseCredential 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); + JsonNode node = fromJwt(jwt, ctx); + return getBuilder(ctx) + .resource(resource) + .jsonData(node) + .jwt(jwt) + .build(); } } 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 index 41ccb2c..8a18c88 100644 --- 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 @@ -7,6 +7,7 @@ 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.AbstractBaseCredential; import org.oneedtech.inspect.vc.Credential; import com.fasterxml.jackson.databind.JsonNode; @@ -20,13 +21,18 @@ import com.google.common.base.Splitter; public abstract class PayloadParser { public abstract boolean supports(ResourceType type); - - public abstract Credential parse(Resource source, RunContext ctx) throws Exception; + + public abstract AbstractBaseCredential parse(Resource source, RunContext ctx) throws Exception; + + @SuppressWarnings("rawtypes") + public static AbstractBaseCredential.Builder getBuilder(RunContext context) { + return ((AbstractBaseCredential.Builder) context.get(RunContext.Key.GENERATED_OBJECT_BUILDER)); + } 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 @@ -34,11 +40,11 @@ public abstract class PayloadParser { 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. + * The entire jwt is stored separately for signature verification later. */ String jwtPayload = new String(decoder.decode(parts.get(1))); @@ -48,5 +54,5 @@ public abstract class PayloadParser { return vcNode; } - + } 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 index f521e47..31ab22a 100644 --- 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 @@ -11,7 +11,7 @@ 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.oneedtech.inspect.vc.AbstractBaseCredential; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -29,12 +29,13 @@ public final class PngParser extends PayloadParser { } @Override - public Credential parse(Resource resource, RunContext ctx) throws Exception { - + public AbstractBaseCredential parse(Resource resource, RunContext ctx) throws Exception { + checkTrue(resource.getType() == ResourceType.PNG); - + try(InputStream is = resource.asByteSource().openStream()) { - + final Keys credentialKey = (Keys) ctx.get(RunContext.Key.PNG_CREDENTIAL_KEY); + ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); imageReader.setInput(ImageIO.createImageInputStream(is), true); IIOMetadata metadata = imageReader.getImageMetadata(0); @@ -43,20 +44,20 @@ public final class PngParser extends PayloadParser { String jwtString = null; String formatSearch = null; JsonNode vcNode = null; - + String[] names = metadata.getMetadataFormatNames(); int length = names.length; for (int i = 0; i < length; i++) { //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0") - formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i])); - if(formatSearch != null) { - vcString = formatSearch; + formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]), credentialKey); + if(formatSearch != null) { + vcString = formatSearch; break; } } - if(vcString == null) { - throw new IllegalArgumentException("No credential inside PNG"); + if(vcString == null) { + throw new IllegalArgumentException("No credential inside PNG"); } vcString = vcString.trim(); @@ -68,29 +69,33 @@ public final class PngParser extends PayloadParser { else { vcNode = fromString(vcString, ctx); } - - return new Credential(resource, vcNode, jwtString); + + return getBuilder(ctx) + .resource(resource) + .jsonData(vcNode) + .jwt(jwtString) + .build(); } } - - private String getOpenBadgeCredentialNodeText(Node node){ + + private String getOpenBadgeCredentialNodeText(Node node, Keys credentialKey){ 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")){ + if(keyword != null && keyword.getNodeValue().equals(credentialKey.getNodeName())){ Node textAttribute = attributes.getNamedItem("text"); - if(textAttribute != null) { - return textAttribute.getNodeValue(); + 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; + String nodeValue = getOpenBadgeCredentialNodeText(child, credentialKey); + if(nodeValue != null) { + return nodeValue; } child = child.getNextSibling(); } @@ -99,4 +104,19 @@ public final class PngParser extends PayloadParser { return null; } + public enum Keys { + OB20("openbadges"), + OB30("openbadgecredential"), + CLR20("clrcredential"); + + private String nodeName; + + private Keys(String nodeName) { + this.nodeName = nodeName; + } + + public String getNodeName() { + return nodeName; + } + } } 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 index f600bc9..34f81fa 100644 --- 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 @@ -11,11 +11,10 @@ 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 org.oneedtech.inspect.vc.AbstractBaseCredential; import com.fasterxml.jackson.databind.JsonNode; @@ -31,49 +30,75 @@ public final class SvgParser extends PayloadParser { } @Override - public Credential parse(Resource resource, RunContext ctx) throws Exception { - + public AbstractBaseCredential parse(Resource resource, RunContext ctx) throws Exception { + final QNames qNames = (QNames) ctx.get(RunContext.Key.SVG_CREDENTIAL_QNAME); + 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(isEndElem(ev, qNames.getCredentialElem())) break; + if(isStartElem(ev, qNames.getCredentialElem())) { + Attribute verifyAttr = ev.asStartElement().getAttributeByName(qNames.getVerifyElem()); if(verifyAttr != null) { String jwt = verifyAttr.getValue(); JsonNode node = fromJwt(jwt, ctx); - return new Credential(resource, node, jwt); + return getBuilder(ctx).resource(resource).jsonData(node).jwt(jwt).build(); } else { while(reader.hasNext()) { ev = reader.nextEvent(); - if(isEndElem(ev, OB_CRED_ELEM)) break; + if(isEndElem(ev, qNames.getCredentialElem())) break; if(ev.getEventType() == XMLEvent.CHARACTERS) { Characters chars = ev.asCharacters(); - if(!chars.isWhiteSpace()) { + if(!chars.isWhiteSpace()) { JsonNode node = fromString(chars.getData(), ctx); - return new Credential(resource, node); + return getBuilder(ctx).resource(resource).jsonData(node).build(); } } - } + } } } } //while(reader.hasNext()) { } - throw new IllegalArgumentException("No credential inside SVG"); - + 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"); + + /* + * Know QNames whre the credential is baked into the SVG + */ + public enum QNames { + OB20(new QName("http://openbadges.org", "assertion"), OB_CRED_VERIFY_ATTR), + OB30(new QName("https://purl.imsglobal.org/ob/v3p0", "credential"), OB_CRED_VERIFY_ATTR), + CLR20(new QName("https://purl.imsglobal.org/clr/v2p0", "credential"), OB_CRED_VERIFY_ATTR); + + private final QName credentialElem; + private final QName verifyElem; + + private QNames(QName credentialElem, QName verifyElem) { + this.credentialElem = credentialElem; + this.verifyElem = verifyElem; + } + + public QName getCredentialElem() { + return credentialElem; + } + + public QName getVerifyElem() { + return verifyElem; + } + + } } 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 index 02a549b..3827e90 100644 --- 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 @@ -8,26 +8,27 @@ 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.AbstractBaseCredential; 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. + * 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) { + if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) { type = TypeDetector.detect(resource, true); if(type.isEmpty()) { //TODO if URI fetch, TypeDetector likely to fail @@ -37,18 +38,18 @@ public class CredentialParseProbe extends Probe { 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); + + AbstractBaseCredential 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/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java index 28b8e7c..1bf3f88 100644 --- 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 @@ -1,10 +1,9 @@ package org.oneedtech.inspect.vc.credential; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; 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; @@ -12,7 +11,7 @@ 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.AbstractBaseCredential; import org.oneedtech.inspect.vc.OB30Inspector; import org.oneedtech.inspect.vc.Samples; import org.oneedtech.inspect.vc.payload.PayloadParser; @@ -21,28 +20,14 @@ import org.oneedtech.inspect.vc.payload.PayloadParserFactory; import com.fasterxml.jackson.databind.ObjectMapper; public class PayloadParserTests { - - @Test + + @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)); + assertNotNull(ext); + AbstractBaseCredential crd = ext.parse(res, mockOB30Context(res)); //System.out.println(crd.getJson().toPrettyString()); assertNotNull(crd); assertNotNull(crd.getJson()); @@ -50,62 +35,76 @@ public class PayloadParserTests { }); } - @Test + @Test + void testSvgJwtExtract() { + assertDoesNotThrow(()->{ + Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG); + PayloadParser ext = PayloadParserFactory.of(res); + assertNotNull(ext); + AbstractBaseCredential 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)); + assertNotNull(ext); + AbstractBaseCredential crd = ext.parse(res, mockOB30Context(res)); //System.out.println(crd.getJson().toPrettyString()); assertNotNull(crd); assertNotNull(crd.getJson()); assertNotNull(crd.getJson().get("@context")); }); } - - @Test + + @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)); + assertNotNull(ext); + AbstractBaseCredential crd = ext.parse(res, mockOB30Context(res)); //System.out.println(crd.getJson().toPrettyString()); assertNotNull(crd); assertNotNull(crd.getJson()); assertNotNull(crd.getJson().get("@context")); }); } - - @Test + + @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)); + assertNotNull(ext); + AbstractBaseCredential crd = ext.parse(res, mockOB30Context(res)); //System.out.println(crd.getJson().toPrettyString()); assertNotNull(crd); assertNotNull(crd.getJson()); assertNotNull(crd.getJson().get("@context")); }); } - - @Test + + @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)); + assertNotNull(ext); + AbstractBaseCredential 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);