From a913ac97d88573a89ff24f353575bca23c9b76b4 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Thu, 23 Jun 2022 11:27:27 -0400 Subject: [PATCH 1/6] Rebaked a couple of badges. Added decode jwt code and png type probe. Refactored accordingly to retain external jwt. Endorsements needs a refactor pending finality on endorsement lookup. --- .../org/oneedtech/inspect/vc/Credential.java | 5 +- .../oneedtech/inspect/vc/OB30Inspector.java | 7 +- .../inspect/vc/probe/CredentialTypeProbe.java | 143 +++++++++++++----- .../org/oneedtech/inspect/vc/OB30Tests.java | 2 - .../src/test/resources/ob30/simple-jwt.png | Bin 83939 -> 84446 bytes .../src/test/resources/ob30/simple-jwt.svg | 2 +- 6 files changed, 117 insertions(+), 42 deletions(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java index 63205c8..4efcf5a 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -25,8 +25,9 @@ public class Credential extends GeneratedObject { final Resource resource; final JsonNode jsonData; final Credential.Type credentialType; + final String jwt; - public Credential(Resource resource, JsonNode data) { + public Credential(Resource resource, JsonNode data, String jwt) { super(ID, GeneratedObject.Type.INTERNAL); checkNotNull(resource, resource.getType(), data); ResourceType type = resource.getType(); @@ -34,10 +35,10 @@ public class Credential extends GeneratedObject { "Unrecognized payload type: " + type.getName()); this.resource = resource; this.jsonData = data; + this.jwt = jwt; ArrayNode typeNode = (ArrayNode)jsonData.get("type"); this.credentialType = Credential.Type.valueOf(typeNode); - } public Resource getResource() { diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index f2fd220..71ca34f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -147,8 +147,11 @@ public class OB30Inspector extends VCInspector { EndorsementInspector subInspector = new EndorsementInspector.Builder().build(); for(JsonNode endorsementNode : endorsements) { probeCount++; - Credential endorsement = new Credential(resource, endorsementNode); - accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); + //TODO: @Markus @Miles, need to refactor to detect as this can be an internal or external proof credential. + //This will LIKELY come from two distinct sources in which case we would detect the type by property name. + //Third param to constructor: Compact JWT -> add third param after decoding. Internal Proof, null jwt string. + //Credential endorsement = new Credential(resource, endorsementNode); + //accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); } } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java index 517116e..bf59280 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java @@ -2,12 +2,16 @@ package org.oneedtech.inspect.vc.probe; import static java.nio.charset.StandardCharsets.UTF_8; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Base64; import java.util.Base64.Decoder; import java.util.List; import java.util.Optional; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.events.Attribute; @@ -22,6 +26,8 @@ import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.detect.TypeDetector; import org.oneedtech.inspect.util.xml.XMLInputFactoryCache; import org.oneedtech.inspect.vc.Credential; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -43,14 +49,19 @@ public class CredentialTypeProbe extends Probe { if(type.isPresent()) { resource.setType(type.get()); + //TODO: Refactor to return the entire credential so we can include optional encoded JWT. if(type.get() == ResourceType.PNG) { - crd = new Credential(resource, fromPNG(resource, context)); + //crd = new Credential(resource, fromPNG(resource, context)); + crd = fromPNG(resource, context); } else if(type.get() == ResourceType.SVG) { - crd = new Credential(resource, fromSVG(resource, context)); + //crd = new Credential(resource, fromSVG(resource, context)); + crd = fromSVG(resource, context); } else if(type.get() == ResourceType.JSON) { - crd = new Credential(resource, fromJson(resource, context)); + //crd = new Credential(resource, fromJson(resource, context)); + crd = fromJson(resource, context); } else if(type.get() == ResourceType.JWT) { - crd = new Credential(resource, fromJWT(resource, context)); + //crd = new Credential(resource, fromJWT(resource, context)); + crd = fromJWT(resource, context); } } @@ -71,12 +82,39 @@ public class CredentialTypeProbe extends Probe { * @param context * @throws Exception */ - private JsonNode fromPNG(Resource resource, RunContext context) throws Exception { - //TODO @Miles - note: iTxt chunk is either plain json or jwt + private Credential fromPNG(Resource resource, RunContext context) throws Exception { try(InputStream is = resource.asByteSource().openStream()) { + ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); + imageReader.setInput(ImageIO.createImageInputStream(is), true); + IIOMetadata metadata = imageReader.getImageMetadata(0); + + String credentialString = null; + String jwtString = null; + String formatSearch = null; + JsonNode credential = null; + String[] names = metadata.getMetadataFormatNames(); + int length = names.length; + for (int i = 0; i < length; i++) + { + //Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0") + formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i])); + if(formatSearch != null) { credentialString = formatSearch; } + } + + if(credentialString == null) { throw new IllegalArgumentException("No credential inside PNG"); } + + credentialString = credentialString.trim(); + if(credentialString.charAt(0) != '{'){ + //This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification. + jwtString = credentialString; + credential = decodeJWT(credentialString,context); + } + else { + credential = buildNodeFromString(credentialString, context); + } + return new Credential(resource, credential, jwtString); } - return null; } /** @@ -84,8 +122,10 @@ public class CredentialTypeProbe extends Probe { * @param context * @throws Exception */ - private JsonNode fromSVG(Resource resource, RunContext context) throws Exception { + private Credential fromSVG(Resource resource, RunContext context) throws Exception { String json = null; + String jwtString = null; + JsonNode credential = null;; try(InputStream is = resource.asByteSource().openStream()) { XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is); while(reader.hasNext()) { @@ -93,7 +133,8 @@ public class CredentialTypeProbe extends Probe { if(ev.isStartElement() && ev.asStartElement().getName().equals(OB_CRED_ELEM)) { Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR); if(verifyAttr != null) { - json = decodeJWT(verifyAttr.getValue()); + jwtString = verifyAttr.getValue(); + credential = decodeJWT(verifyAttr.getValue(), context); break; } else { while(reader.hasNext()) { @@ -105,58 +146,90 @@ public class CredentialTypeProbe extends Probe { Characters chars = ev.asCharacters(); if(!chars.isWhiteSpace()) { json = chars.getData(); + credential = buildNodeFromString(json, context); break; } } - } - } - } - if(json!=null) break; + } + } + } + if(credential!=null) break; } } - if(json == null) throw new IllegalArgumentException("No credential inside SVG"); - return fromString(json, context); + if(credential == null) throw new IllegalArgumentException("No credential inside SVG"); + return new Credential(resource, credential, jwtString); } /** - * Create a JsonNode object from a raw JSON resource. + * Create a Credential object from a raw JSON resource. * @param context */ - private JsonNode fromJson(Resource resource, RunContext context) throws Exception { - return fromString(resource.asByteSource().asCharSource(UTF_8).read(), context); + private Credential fromJson(Resource resource, RunContext context) throws Exception { + return new Credential(resource, buildNodeFromString(resource.asByteSource().asCharSource(UTF_8).read(), context), null); } - + + /** + * Create a Credential object from a JWT resource. + * @param context + */ + private Credential fromJWT(Resource resource, RunContext context) throws Exception { + return new Credential( + resource, + decodeJWT( + resource.asByteSource().asCharSource(UTF_8).read(), + context + ) + , resource.asByteSource().asCharSource(UTF_8).read() + ); + } + /** * Create a JsonNode object from a String. */ - private JsonNode fromString(String json, RunContext context) throws Exception { + private JsonNode buildNodeFromString(String json, RunContext context) throws Exception { return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json); } - /** - * Create a JsonNode object from a JWT resource. - * @param context - */ - private JsonNode fromJWT(Resource resource, RunContext context) throws Exception { - return fromString(decodeJWT(resource.asByteSource().asCharSource(UTF_8).read()), context); - } - /** * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof * @return The decoded JSON String */ - private String decodeJWT(String jwt) { + private JsonNode decodeJWT(String jwt, RunContext context) throws Exception { List parts = Splitter.on('.').splitToList(jwt); if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); final Decoder decoder = Base64.getUrlDecoder(); - String joseHeader = new String(decoder.decode(parts.get(0))); + //For this step we are only deserializing the stored badge out of the payload. The entire jwt is stored separately for + //signature verification later. String jwtPayload = new String(decoder.decode(parts.get(1))); - String jwsSignature = new String(decoder.decode(parts.get(2))); - - //TODO @Miles - - return null; + + //Deserialize and fetch the 'vc' node from the object + JsonNode outerPayload = buildNodeFromString(jwtPayload, context); + JsonNode vcNode = outerPayload.get("vc"); + + return vcNode; + } + + private String getOpenBadgeCredentialNodeText(Node node){ + NamedNodeMap attributes = node.getAttributes(); + + //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one. + if(attributes.getNamedItem("keyword") != null && attributes.getNamedItem("keyword").getNodeValue().equals("openbadgecredential")){ + Node textAttribute = attributes.getNamedItem("text"); + if(textAttribute != null) { return textAttribute.getNodeValue(); } + } + + //iterate over all children depth first and search for the credential node. + Node child = node.getFirstChild(); + while (child != null) + { + String nodeValue = getOpenBadgeCredentialNodeText(child); + if(nodeValue != null) { return nodeValue; } + child = child.getNextSibling(); + } + + //Return null if we haven't found anything at this recursive depth. + return null; } private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential"); diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java index 1083aed..4426250 100644 --- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java +++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB30Tests.java @@ -32,7 +32,6 @@ public class OB30Tests { }); } - @Disabled @Test void testSimplePNGPlainValid() { assertDoesNotThrow(()->{ @@ -42,7 +41,6 @@ public class OB30Tests { }); } - @Disabled @Test void testSimplePNGJWTValid() { assertDoesNotThrow(()->{ diff --git a/inspector-vc/src/test/resources/ob30/simple-jwt.png b/inspector-vc/src/test/resources/ob30/simple-jwt.png index 47e5afc81c95f18cdf6c6f99dcb22a541e147c9b..84a9ab2fa642d0a39d8779b00ce66e1bfbd1505f 100644 GIT binary patch delta 900 zcmWkpNv@+t0L@RUv&aSd6kjU9V6*6!j2R4OFhx}{Y#c*jV<;x$ZyPp_a)DNqMQ)K* zOF2o@yKJ(`Iv0rh?cUSV8~*wH{_o%3|NQ0UFXj6m{`>u_^783n4rbj6`_Yv0(PC!V zo_wM!{^^^30eN6bCbN(qeB-l|cd|MMvC?6X1z{%!rM2?IM9L7Ytn+Q_NXSRvqt2lk~LF6MW(>gcZ6U z>2R-O9viY9GUd%NtQKN_hAWOG%mFb<4lZvs385WDR7mR%ph;&gPhvZQcNgn7f68XL zK@A+rT3#0%j^J|)Sv#d-J@yQ?ke7$hU^k>u+o>-tDz(wZ?L8*X#n|1)q!oQ2V^oP# zqPju+{78*Lp{grU)v_ZX55a_Q1Q0W>h)622r!1Q?lEgj-MFQN2axoXyu0U1YiG=0l z=`}FV9swlBTMF3ORTK0q0;yVoOmKxTsz89qH4iUMPKwUO_c+y(5)ln}1bRu;U9lS9 zAdz`jRgAECenv`()_NUOg@;kGf99?UpLAIW_Zjb2K3I%dWZ4LlF(umR5WRpuKdfF3 z?;hC5WUt)cpkdrMq_t$~JDbHxR2}d!>)GG+K zmE4v_IbFv2mh(0WOqKPrUrTRw92iA)M?!z zyTFYU&2BgUtZVtwT$*pnYwxl7n(V{t>8{_IY8B5rXf zZnWk#)0ej3U8x^3@xYFX>p8=CXGPa1!d$6_z|jMowXxcx{Ipm}zQGk7KU<&qB3oWU zhxF2vhDCAa1>kAc6(bkd0ZYYsC{g6FeHUH>|)!L&T%Z76?@X1k~G1J`g~)Zj1g^(+%%^;9grA0nD%;0V*7d z&PI^rht?4oaL%4H?-yRtp>u+OR_a>~W@9bYp - + From 98e00ccf4890d0ce4f5465a10bfe6c16ccf9df98 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:16:28 -0400 Subject: [PATCH 2/6] WIP, signature additions. --- inspector-vc/pom.xml | 25 +++++++ .../org/oneedtech/inspect/vc/Credential.java | 4 ++ .../oneedtech/inspect/vc/OB30Inspector.java | 11 +-- .../inspect/vc/probe/CredentialTypeProbe.java | 5 -- .../vc/probe/SignatureVerifierProbe.java | 70 ++++++++++++++++++- 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 6881751..396f3d5 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -10,6 +10,31 @@ org.1edtech inspector-core + + + org.bouncycastle + bcpkix-jdk15on + 1.58 + + + com.auth0 + auth0 + 1.20.0 + + + com.auth0 + jwks-rsa + 0.12.0 + + + com.auth0 + java-jwt + 3.10.3 + + + org.springframework + spring-core + 5.0.12.RELEASE \ No newline at end of file diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java index 4efcf5a..3e73655 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Credential.java @@ -52,6 +52,10 @@ public class Credential extends GeneratedObject { public Credential.Type getCredentialType() { return credentialType; } + + public String getJwt() { + return jwt; + } /** * Get the canonical schema for this credential if such exists. diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index 71ca34f..da95e80 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -6,6 +6,7 @@ import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY; import static org.oneedtech.inspect.vc.util.JsonNodeUtil.getEndorsements; +import static com.google.common.base.Strings.isNullOrEmpty; import java.net.URI; import java.util.ArrayList; @@ -105,10 +106,12 @@ public class OB30Inspector extends VCInspector { probeCount++; accumulator.add(new InlineJsonSchemaProbe().run(crd, ctx)); - //verify signatures TODO @Miles - probeCount++; - accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + //If this credential was originally contained in a JWT we must validate the jwt and external proof. + if(!isNullOrEmpty(crd.getJwt())){ + probeCount++; + accumulator.add(new SignatureVerifierProbe().run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } //verify proofs TODO @Miles probeCount++; diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java index bf59280..535c9c0 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialTypeProbe.java @@ -49,18 +49,13 @@ public class CredentialTypeProbe extends Probe { if(type.isPresent()) { resource.setType(type.get()); - //TODO: Refactor to return the entire credential so we can include optional encoded JWT. if(type.get() == ResourceType.PNG) { - //crd = new Credential(resource, fromPNG(resource, context)); crd = fromPNG(resource, context); } else if(type.get() == ResourceType.SVG) { - //crd = new Credential(resource, fromSVG(resource, context)); crd = fromSVG(resource, context); } else if(type.get() == ResourceType.JSON) { - //crd = new Credential(resource, fromJson(resource, context)); crd = fromJson(resource, context); } else if(type.get() == ResourceType.JWT) { - //crd = new Credential(resource, fromJWT(resource, context)); crd = fromJWT(resource, context); } } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java index d9961fa..08ad567 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java @@ -1,9 +1,30 @@ package org.oneedtech.inspect.vc.probe; +import static com.google.common.base.Strings.isNullOrEmpty; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.List; +import java.util.Base64; +import java.util.Base64.Decoder; + import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.vc.Credential; +import org.springframework.util.Base64Utils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Splitter; /** * A Probe that verifies credential signatures @@ -17,11 +38,56 @@ public class SignatureVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { - - //TODO @Miles -- if sigs fail, report OutCome.Fatal + try { + testingSignatureValidationCode(crd); + } catch (Exception e) { + return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); + } return success(ctx); } + + private void testingSignatureValidationCode(Credential crd) throws Exception { + DecodedJWT decodedJwt = null; + String jwt = crd.getJwt(); + if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid jwt"); + List parts = Splitter.on('.').splitToList(jwt); + if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); + + final Decoder decoder = Base64.getUrlDecoder(); + String joseHeader = new String(decoder.decode(parts.get(0))); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode headerObj = mapper.readTree(joseHeader); + + //MUST be "RS256" + + //Option 1, fetch directly from header + JsonNode jwk = headerObj.get("jwk"); + + //Option 2, fetch from a hosting url + JsonNode kid = headerObj.get("kid"); + + if(jwk == null && kid == null) { throw new Exception("asdf"); } + if(kid != null){ + //TODO @Miles load jwk JsonNode from url and do the rest the same below. Need to set up testing. + String kidUrl = kid.textValue(); + } + + //Clean up may be required. Currently need to cleanse extra double quoting. + String modulusString = jwk.get("n").textValue(); + String exponentString = jwk.get("e").textValue(); + + BigInteger modulus = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(modulusString)); + BigInteger exponent = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(exponentString)); + + PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent)); + + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null); + JWTVerifier verifier = JWT.require(algorithm) + .build(); //Reusable verifier instance + decodedJwt = verifier.verify(jwt); + } public static final String ID = SignatureVerifierProbe.class.getSimpleName(); From 78456fceb81d34ce9b7dbcdbf5f55a876ce36cc7 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:23:30 -0400 Subject: [PATCH 3/6] Added 'alg' check. --- .../oneedtech/inspect/vc/probe/SignatureVerifierProbe.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java index 08ad567..91f4f64 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java @@ -61,6 +61,10 @@ public class SignatureVerifierProbe extends Probe { JsonNode headerObj = mapper.readTree(joseHeader); //MUST be "RS256" + JsonNode alg = headerObj.get("alg"); + if(alg == null || !alg.textValue().equals("RS256")) { throw new Exception("alg must be present and must be 'RS256'"); } + + //TODO: decoded jwt will check timestamps, but shall we explicitly break these out? //Option 1, fetch directly from header JsonNode jwk = headerObj.get("jwk"); @@ -68,7 +72,7 @@ public class SignatureVerifierProbe extends Probe { //Option 2, fetch from a hosting url JsonNode kid = headerObj.get("kid"); - if(jwk == null && kid == null) { throw new Exception("asdf"); } + if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); } if(kid != null){ //TODO @Miles load jwk JsonNode from url and do the rest the same below. Need to set up testing. String kidUrl = kid.textValue(); From c2540377e2cdf77ef4096d3228e8a4319a118bb9 Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Fri, 24 Jun 2022 15:45:27 -0400 Subject: [PATCH 4/6] Updated bouncy castle to a higher version to support ED25529. Pulled in Stack Overflow signature/proof verification. --- inspector-vc/pom.xml | 6 ++--- .../oneedtech/inspect/vc/OB30Inspector.java | 9 ++++--- .../inspect/vc/probe/ProofVerifierProbe.java | 27 +++++++++++++++++++ .../vc/probe/SignatureVerifierProbe.java | 24 ++++++++++++++--- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml index 396f3d5..f253160 100644 --- a/inspector-vc/pom.xml +++ b/inspector-vc/pom.xml @@ -11,10 +11,10 @@ org.1edtech inspector-core - + org.bouncycastle - bcpkix-jdk15on - 1.58 + bcprov-jdk15to18 + 1.65 com.auth0 diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java index da95e80..d68c7e8 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB30Inspector.java @@ -114,9 +114,12 @@ public class OB30Inspector extends VCInspector { } //verify proofs TODO @Miles - probeCount++; - accumulator.add(new ProofVerifierProbe().run(crd, ctx)); - if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + //If this credential was not contained in a jwt it must have an internal proof. + if(isNullOrEmpty(crd.getJwt())){ + probeCount++; + accumulator.add(new ProofVerifierProbe().run(crd, ctx)); + if(broken(accumulator)) return abort(ctx, accumulator, probeCount); + } //check refresh service if we are not already refreshed probeCount++; diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java index e506aec..1618148 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ProofVerifierProbe.java @@ -1,5 +1,15 @@ package org.oneedtech.inspect.vc.probe; +import java.security.KeyFactory; +import java.security.Security; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; + +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.encoders.Hex; import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.report.ReportItems; @@ -23,5 +33,22 @@ public class ProofVerifierProbe extends Probe { return success(ctx); } + public boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception { + //TODO: continue this implementation. + //Pulled in bouncy castle library and made sure this sample compiled only. + final var provider = new BouncyCastleProvider(); + Security.addProvider(provider); + final var byteKey = Hex.decode(pubkey); + final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), byteKey); + final var pkSpec = new X509EncodedKeySpec(pki.getEncoded()); + final var kf = KeyFactory.getInstance("ed25519", provider); + final var publicKey = kf.generatePublic(pkSpec); + final var signedData = Signature.getInstance("ed25519", provider); + signedData.initVerify(publicKey); + signedData.update(timestamp.getBytes()); + signedData.update(message.getBytes()); + return signedData.verify(Hex.decode(signature)); + } + public static final String ID = ProofVerifierProbe.class.getSimpleName(); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java index 91f4f64..e8c06ae 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/SignatureVerifierProbe.java @@ -21,6 +21,10 @@ import org.springframework.util.Base64Utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.AlgorithmMismatchException; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -39,7 +43,7 @@ public class SignatureVerifierProbe extends Probe { @Override public ReportItems run(Credential crd, RunContext ctx) throws Exception { try { - testingSignatureValidationCode(crd); + verifySignature(crd); } catch (Exception e) { return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); } @@ -47,7 +51,7 @@ public class SignatureVerifierProbe extends Probe { return success(ctx); } - private void testingSignatureValidationCode(Credential crd) throws Exception { + private void verifySignature(Credential crd) throws Exception { DecodedJWT decodedJwt = null; String jwt = crd.getJwt(); if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid jwt"); @@ -90,7 +94,21 @@ public class SignatureVerifierProbe extends Probe { Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null); JWTVerifier verifier = JWT.require(algorithm) .build(); //Reusable verifier instance - decodedJwt = verifier.verify(jwt); + try { + decodedJwt = verifier.verify(jwt); + } + catch(SignatureVerificationException ex){ + throw new Exception("JWT Invalid signature", ex); + } + catch(AlgorithmMismatchException ex){ + throw new Exception("JWT Algorithm mismatch", ex); + } + catch(TokenExpiredException ex){ + throw new Exception("JWT Token expired", ex); + } + catch(InvalidClaimException ex){ + throw new Exception("JWT, one or more claims are invalid", ex); + } } public static final String ID = SignatureVerifierProbe.class.getSimpleName(); From e09a3830975d90806607add41b1bc401971926ce Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Wed, 29 Jun 2022 16:08:18 -0400 Subject: [PATCH 5/6] One additional commit which is just to update our simple.json proof with a sample with a key directly in the file. --- .../src/test/resources/ob30/simple.json | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/inspector-vc/src/test/resources/ob30/simple.json b/inspector-vc/src/test/resources/ob30/simple.json index cdfffdb..50e33aa 100644 --- a/inspector-vc/src/test/resources/ob30/simple.json +++ b/inspector-vc/src/test/resources/ob30/simple.json @@ -1,36 +1,36 @@ { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://imsglobal.github.io/openbadges-specification/context.json", - "https://w3id.org/security/suites/ed25519-2020/v1" - ], - "id": "http://example.edu/credentials/3732", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", "type": [ - "VerifiableCredential", - "OpenBadgeCredential" + "Profile" ], - "issuer": { - "id": "https://example.edu/issuers/565049", - "type": [ - "Profile" - ], - "name": "Example University" - }, - "issuanceDate": "2010-01-01T00:00:00Z", - "name": "Example University Degree", - "credentialSubject": { - "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", - "type": [ - "AchievementSubject" - ] - }, - "proof": [ - { - "type": "Ed25519Signature2020", - "created": "2022-06-09T22:56:28Z", - "verificationMethod": "https://example.edu/issuers/565049#key-1", - "proofPurpose": "assertionMethod", - "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN" - } + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" ] - } \ No newline at end of file + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-28T16:28:36Z", + "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj", + "proofPurpose": "assertionMethod", + "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM" + } + ] +} \ No newline at end of file From b32e81bed5621fb57dec59f23fed47e092c4ddaa Mon Sep 17 00:00:00 2001 From: Miles Lyon Date: Wed, 29 Jun 2022 16:09:35 -0400 Subject: [PATCH 6/6] Leaving the old version in case it is needed. We can delete later. --- .../resources/ob30/simple-old-delete.json | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 inspector-vc/src/test/resources/ob30/simple-old-delete.json diff --git a/inspector-vc/src/test/resources/ob30/simple-old-delete.json b/inspector-vc/src/test/resources/ob30/simple-old-delete.json new file mode 100644 index 0000000..7795327 --- /dev/null +++ b/inspector-vc/src/test/resources/ob30/simple-old-delete.json @@ -0,0 +1,36 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://imsglobal.github.io/openbadges-specification/context.json", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example University" + }, + "issuanceDate": "2010-01-01T00:00:00Z", + "name": "Example University Degree", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + }, + "proof": [ + { + "type": "Ed25519Signature2020", + "created": "2022-06-09T22:56:28Z", + "verificationMethod": "https://example.edu/issuers/565049#key-1", + "proofPurpose": "assertionMethod", + "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN" + } + ] + } \ No newline at end of file