add iron for vc verification, refactor+cleanup
This commit is contained in:
parent
092d4eb8f1
commit
9d3110547e
@ -1,20 +1,17 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.1edtech</groupId>
|
||||
<artifactId>inspector</artifactId>
|
||||
<version>0.9.2</version>
|
||||
</parent>
|
||||
<artifactId>inspector-vc</artifactId>
|
||||
<dependencies>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.1edtech</groupId>
|
||||
<artifactId>inspector</artifactId>
|
||||
<version>0.9.2</version>
|
||||
</parent>
|
||||
<artifactId>inspector-vc</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.1edtech</groupId>
|
||||
<artifactId>inspector-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15to18</artifactId>
|
||||
<version>1.65</version>
|
||||
<artifactId>inspector-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
@ -32,17 +29,10 @@
|
||||
<version>3.10.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<version>5.0.12.RELEASE</version>
|
||||
<groupId>com.apicatalog</groupId>
|
||||
<artifactId>iron-verifiable-credentials-jre8</artifactId>
|
||||
<version>0.7.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google</groupId>
|
||||
<artifactId>bitcoinj</artifactId>
|
||||
<version>0.11.3</version>
|
||||
</dependency>
|
||||
<!-- Thanks for using https://jar-download.com -->
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/com.apicatalog/titanium-json-ld -->
|
||||
<dependency>
|
||||
<groupId>com.apicatalog</groupId>
|
||||
@ -50,7 +40,7 @@
|
||||
<version>1.3.1</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/io.setl/rdf-urdna -->
|
||||
<!-- https://github.com/setl/rdf-urdna -->
|
||||
<!-- https://github.com/setl/rdf-urdna
|
||||
<dependency>
|
||||
<groupId>io.setl</groupId>
|
||||
<artifactId>rdf-urdna</artifactId>
|
||||
@ -65,7 +55,8 @@
|
||||
<artifactId>titanium-json-ld</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.glassfish</groupId>
|
||||
<artifactId>jakarta.json</artifactId>
|
||||
|
@ -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<String> 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<ResourceType> RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT);
|
||||
|
||||
}
|
||||
|
@ -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<ReportItems> 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<JsonNode> 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<String> 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<JsonNode> 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<Credential> 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<JsonNode> 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<Credential> 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<OB30Inspector.Builder> {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<String> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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<PayloadParser> 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
@ -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<Resource> {
|
||||
|
||||
@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<ResourceType> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Resource> {
|
||||
|
||||
@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<ResourceType> 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<String> 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");
|
||||
}
|
@ -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<Credential> {
|
||||
public class InlineJsonSchemaProbe extends Probe<JsonNode> {
|
||||
private static final Set<String> 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<ReportItems> accumulator = new ArrayList<>();
|
||||
Set<String> ioErrors = new HashSet<>();
|
||||
|
||||
@ -37,7 +42,7 @@ public class InlineJsonSchemaProbe extends Probe<Credential> {
|
||||
// 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<Credential> {
|
||||
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<Credential> {
|
||||
return new ReportItems(accumulator);
|
||||
}
|
||||
|
||||
private boolean equals(Optional<SchemaKey> 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();
|
||||
|
@ -34,7 +34,7 @@ public class IssuanceVerifierProbe extends Probe<Credential> {
|
||||
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());
|
||||
|
@ -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<JsonNode> value = new Predicate<>() {
|
||||
@Override
|
||||
public boolean test(JsonNode node) {
|
||||
List<String> values = asStringList(node);
|
||||
for(String exp : exp) {
|
||||
if(values.contains(exp)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private static final List<String> 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<JsonNode> value = new Predicate<>() {
|
||||
@Override
|
||||
public boolean test(JsonNode node) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -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<Credential> {
|
||||
|
||||
/*
|
||||
* 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<Entry<String,JsonNode>> iter = proof.fields();
|
||||
while (iter.hasNext()) {
|
||||
Entry<String,JsonNode> 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();
|
||||
}
|
||||
|
@ -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<Credential> {
|
||||
@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<String> 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<Credential> {
|
||||
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);
|
||||
|
@ -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<JsonNode> {
|
||||
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<String> 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();
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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<JsonNode> getEndorsements(JsonNode root, JsonPathEvaluator jsonPath) {
|
||||
public static List<JsonNode> asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) {
|
||||
List<JsonNode> 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<String> asStringList(JsonNode node) {
|
||||
if(!(node instanceof ArrayNode)) {
|
||||
return List.of(node.asText());
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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<JsonNode> list = JsonNodeUtil.getEndorsements(root, jsonPath);
|
||||
void testFlattenNodeList() {
|
||||
Assertions.assertDoesNotThrow(()->{
|
||||
String json = Samples.OB30.JSON.COMPLETE_JSON.asString();
|
||||
JsonNode root = mapper.readTree(json);
|
||||
List<JsonNode> 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);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
37
inspector-vc/src/test/resources/ob30/simple-expired.json
Normal file
37
inspector-vc/src/test/resources/ob30/simple-expired.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
37
inspector-vc/src/test/resources/ob30/simple-issued.json
Normal file
37
inspector-vc/src/test/resources/ob30/simple-issued.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
36
inspector-vc/src/test/resources/ob30/simple-proof-error.json
Normal file
36
inspector-vc/src/test/resources/ob30/simple-proof-error.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user