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();