diff --git a/inspector-vc/pom.xml b/inspector-vc/pom.xml
index e8ab3e8..a5afb84 100644
--- a/inspector-vc/pom.xml
+++ b/inspector-vc/pom.xml
@@ -5,7 +5,7 @@
org.1edtech
inspector
- 0.9.8
+ 0.9.9
inspector-vc
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java
new file mode 100644
index 0000000..3f02a85
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java
@@ -0,0 +1,349 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.vc.util.PrimitiveValueValidator.validateIri;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.oneedtech.inspect.schema.Catalog;
+import org.oneedtech.inspect.schema.SchemaKey;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.vc.Validation.MessageLevel;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * A wrapper object for a OB 2.0 assertion. This contains e.g. the origin resource
+ * and the extracted JSON data plus any other stuff Probes need.
+ * @author xaracil
+ */
+public class Assertion extends Credential {
+
+ final Assertion.Type assertionType;
+
+ protected Assertion(Resource resource, JsonNode data, String jwt, Map schemas, String issuedOnPropertyName, String expiresAtPropertyName) {
+ super(resource.getID(), resource, data, jwt, schemas, issuedOnPropertyName, expiresAtPropertyName);
+
+ JsonNode typeNode = jsonData.get("type");
+ this.assertionType = Assertion.Type.valueOf(typeNode);
+ }
+
+ @Override
+ public CredentialEnum getCredentialType() {
+ return assertionType;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("super", super.toString())
+ .add("assertionType", assertionType)
+ .toString();
+ }
+
+ public List getValidations() {
+ return validationMap.get(assertionType);
+ }
+
+ private static final Map schemas = new ImmutableMap.Builder()
+ .put(Type.Assertion, Catalog.OB_21_ASSERTION_JSON)
+ .build();
+
+ public static class Builder extends Credential.Builder {
+
+ @Override
+ public Assertion build() {
+ // transform key of schemas map to string because the type of the key in the base map is generic
+ // and our specific key is an Enum
+ return new Assertion(getResource(), getJsonData(), getJwt(), schemas, ISSUED_ON_PROPERTY_NAME, EXPIRES_AT_PROPERTY_NAME);
+ }
+ }
+
+ public enum Type implements CredentialEnum {
+ Assertion(List.of("Assertion")),
+ BadgeClass(List.of("BadgeClass")),
+
+ AlignmentObject(List.of("AlignmentObject")),
+ Criteria(List.of("Criteria")),
+ CryptographicKey(List.of("CryptographicKey")),
+ Endorsement(List.of("Endorsement")),
+ EndorsementClaim(List.of("EndorsementClaim")),
+ Evidence(List.of("Evidence")),
+ ExpectedRecipientProfile(List.of("ExpectedRecipientProfile")),
+ Extension(List.of("Extension")),
+ IdentityObject(List.of("IdentityObject")),
+ Image(List.of("Image")),
+ Issuer(List.of("Issuer")),
+ Profile(List.of("Profile")),
+ RevocationList(List.of("RevocationList")),
+ VerificationObject(List.of("VerificationObject")),
+ VerificationObjectAssertion(List.of("VerificationObjectAssertion")),
+ VerificationObjectIssuer(List.of("VerificationObjectIssuer")),
+ External(Collections.emptyList(), false),
+ Unknown(Collections.emptyList());
+
+ public static List primaryObjects = List.of(Assertion, BadgeClass, Issuer, Profile, Endorsement);
+
+ private final List allowedTypeValues;
+ private final boolean allowedTypeValuesRequired;
+
+ Type(List typeValues) {
+ this(typeValues, true);
+ }
+
+ Type(List typeValues, boolean allowedTypeValuesRequired) {
+ this.allowedTypeValues = typeValues;
+ this.allowedTypeValuesRequired = allowedTypeValuesRequired;
+ }
+
+ public static Assertion.Type valueOf (JsonNode typeNode) {
+ if(typeNode != null) {
+ List values = JsonNodeUtil.asStringList(typeNode);
+ for (String value : values) {
+ Type found = Arrays.stream(Type.values())
+ .filter(type -> value.equals(type.toString()))
+ .findFirst()
+ .orElse(Unknown);
+ if (found != Unknown) {
+ return found;
+ }
+ }
+ }
+
+ // check external type
+ if (validateIri(typeNode)) {
+ return External;
+ }
+
+ return Unknown;
+ }
+
+ @Override
+ public List getRequiredTypeValues() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getAllowedTypeValues() {
+ return allowedTypeValues;
+ }
+
+ @Override
+ public List getContextUris() {
+ return List.of("https://w3id.org/openbadges/v2") ;
+ }
+
+ @Override
+ public boolean isAllowedTypeValuesRequired() {
+ return allowedTypeValuesRequired;
+ }
+
+ public List getValidations() {
+ return validationMap.get(this);
+ }
+ }
+
+ public enum ValueType {
+ BOOLEAN(PrimitiveValueValidator::validateBoolean),
+ COMPACT_IRI(PrimitiveValueValidator::validateCompactIri),
+ DATA_URI(PrimitiveValueValidator::validateDataUri),
+ DATA_URI_OR_URL(PrimitiveValueValidator::validateDataUriOrUrl),
+ DATETIME(PrimitiveValueValidator::validateDatetime),
+ EMAIL(PrimitiveValueValidator::validateEmail),
+ ID(null),
+ IDENTITY_HASH(PrimitiveValueValidator::validateIdentityHash),
+ IRI(PrimitiveValueValidator::validateIri),
+ LANGUAGE(PrimitiveValueValidator::validateLanguage),
+ MARKDOWN_TEXT(PrimitiveValueValidator::validateMarkdown),
+ RDF_TYPE(PrimitiveValueValidator::validateRdfType),
+ TELEPHONE(PrimitiveValueValidator::validateTelephone),
+ TEXT(PrimitiveValueValidator::validateText),
+ TEXT_OR_NUMBER(PrimitiveValueValidator::validateTextOrNumber),
+ URL(PrimitiveValueValidator::validateUrl),
+ URL_AUTHORITY(PrimitiveValueValidator::validateUrlAuthority),
+
+ IMAGE(null),
+ ISSUER(null);
+
+ private final Function validationFunction;
+
+ private ValueType(Function validationFunction) {
+ this.validationFunction = validationFunction;
+ }
+
+ public Function getValidationFunction() {
+ return validationFunction;
+ }
+
+ public static List primitives = List.of(BOOLEAN, DATA_URI_OR_URL, DATETIME, ID, IDENTITY_HASH, IRI, LANGUAGE, MARKDOWN_TEXT,
+ TELEPHONE, TEXT, TEXT_OR_NUMBER, URL, URL_AUTHORITY);
+ }
+
+ public static Map> validationMap = new ImmutableMap.Builder>()
+ .put(Type.Assertion, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.Assertion)).build(),
+ new Validation.Builder().name("recipient").type(ValueType.ID).expectedType(Type.IdentityObject).required(true).build(),
+ new Validation.Builder().name("badge").type(ValueType.ID).expectedType(Type.BadgeClass).fetch(true).required(true).allowFlattenEmbeddedResource(true).build(),
+ new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectAssertion).required(true).build(),
+ new Validation.Builder().name("issuedOn").type(ValueType.DATETIME).required(true).build(),
+ new Validation.Builder().name("expires").type(ValueType.DATETIME).build(),
+ new Validation.Builder().name("image").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Image).build(),
+ new Validation.Builder().name("narrative").type(ValueType.MARKDOWN_TEXT).build(),
+ new Validation.Builder().name("evidence").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Evidence).many(true).build(),
+ new Validation.Builder().name("image").type(ValueType.IMAGE).build(),
+ new Validation.Builder().name("@language").type(ValueType.LANGUAGE).build(),
+ new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).build(),
+ new Validation.Builder().name("related").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Assertion).many(true).fullValidate(false).build(),
+ new Validation.Builder().name("endorsement").type(ValueType.ID).allowRemoteUrl(true).fetch(true).expectedType(Type.Endorsement).many(true).build()
+ ))
+ .put(Type.BadgeClass, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.BadgeClass)).build(),
+ new Validation.Builder().name("issuer").type(ValueType.ID).expectedType(Type.Profile).fetch(true).required(true).allowFlattenEmbeddedResource(true).build(),
+ new Validation.Builder().name("name").type(ValueType.TEXT).required(true).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).required(true).build(),
+ new Validation.Builder().name("image").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Image).allowDataUri(true).build(),
+ new Validation.Builder().name("criteria").type(ValueType.ID).expectedType(Type.Criteria).required(true).allowRemoteUrl(true).build(),
+ new Validation.Builder().name("alignment").type(ValueType.ID).expectedType(Type.AlignmentObject).many(true).build(),
+ new Validation.Builder().name("tags").type(ValueType.TEXT).many(true).build(),
+ new Validation.Builder().name("@language").type(ValueType.LANGUAGE).build(),
+ new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).build(),
+ new Validation.Builder().name("related").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.BadgeClass).many(true).fullValidate(false).build(),
+ new Validation.Builder().name("endorsement").type(ValueType.ID).allowRemoteUrl(true).fetch(true).expectedType(Type.Endorsement).many(true).build(),
+ new Validation.Builder().name("image").type(ValueType.IMAGE).allowDataUri(true).build()
+ ))
+ .put(Type.AlignmentObject, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType(Type.AlignmentObject).build(),
+ new Validation.Builder().name("targetName").type(ValueType.TEXT).required(true).build(),
+ new Validation.Builder().name("targetUrl").type(ValueType.URL).required(true).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("targetFramework").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("targetCode").type(ValueType.TEXT).build()
+ ))
+ .put(Type.Criteria, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType(Type.Criteria).build(),
+ new Validation.Builder().name("id").type(ValueType.IRI).build(),
+ new Validation.Builder().name("narrative").type(ValueType.MARKDOWN_TEXT).build()
+ ))
+ .put(Type.CryptographicKey, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType(Type.CryptographicKey).build(),
+ new Validation.Builder().name("owner").type(ValueType.IRI).fetch(true).build(),
+ new Validation.Builder().name("publicKeyPem").type(ValueType.TEXT).build()
+ ))
+ .put(Type.Endorsement, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.Endorsement)).build(),
+ new Validation.Builder().name("claim").type(ValueType.ID).required(true).expectedTypes(List.of(Type.EndorsementClaim, Type.Endorsement)).fullValidate(false).build(),
+ new Validation.Builder().name("issuedOn").type(ValueType.DATETIME).required(true).build(),
+ new Validation.Builder().name("issuer").type(ValueType.ID).expectedType(Type.Profile).fetch(true).required(true).build(),
+ new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectAssertion).required(true).build(),
+ new Validation.Builder().name("@language").type(ValueType.LANGUAGE).build(),
+ new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).build(),
+ new Validation.Builder().name("related").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Endorsement).many(true).fullValidate(false).build()
+ ))
+ .put(Type.EndorsementClaim, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("endorsementComment").type(ValueType.MARKDOWN_TEXT).build()
+ ))
+ .put(Type.Evidence, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType(Type.Evidence).build(),
+ new Validation.Builder().name("id").type(ValueType.IRI).build(),
+ new Validation.Builder().name("narrative").type(ValueType.MARKDOWN_TEXT).build(),
+ new Validation.Builder().name("name").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("genre").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("audience").type(ValueType.TEXT).build()
+ ))
+ .put(Type.ExpectedRecipientProfile, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).mustContainOneType(List.of(Type.Issuer, Type.Profile)).defaultType(Type.Profile).build(),
+ new Validation.Builder().name("name").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("image").type(ValueType.ID).expectedType(Type.Image).allowDataUri(true).build(),
+ new Validation.Builder().name("url").type(ValueType.URL).many(true).build(),
+ new Validation.Builder().name("email").type(ValueType.EMAIL).many(true).build(),
+ new Validation.Builder().name("telephone").type(ValueType.TELEPHONE).many(true).build(),
+ new Validation.Builder().name("publicKey").type(ValueType.ID).many(true).expectedType(Type.CryptographicKey).build(),
+ new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectIssuer).build()
+ ))
+ .put(Type.Extension, List.of())
+ .put(Type.IdentityObject, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).mustContainOne(List.of("id", "email", "url", "telephone")).build(),
+ new Validation.Builder().name("identity").type(ValueType.IDENTITY_HASH).required(true).build(),
+ new Validation.Builder().name("hashed").type(ValueType.BOOLEAN).required(true).build(),
+ new Validation.Builder().name("salt").type(ValueType.TEXT).build()
+ ))
+ .put(Type.Image, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType("schema:ImageObject").build(),
+ new Validation.Builder().name("id").type(ValueType.DATA_URI_OR_URL).required(true).build(),
+ new Validation.Builder().name("caption").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("author").type(ValueType.IRI).build(),
+ new Validation.Builder().name("id").type(ValueType.IMAGE).allowDataUri(true).build()
+ ))
+ .put(Type.Issuer, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.Issuer, Type.Profile)).build(),
+ new Validation.Builder().name("name").type(ValueType.TEXT).required(true).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("image").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Image).allowDataUri(true).build(),
+ new Validation.Builder().name("url").type(ValueType.URL).required(true).build(),
+ new Validation.Builder().name("email").type(ValueType.EMAIL).required(true).build(),
+ new Validation.Builder().name("telephone").type(ValueType.TELEPHONE).build(),
+ new Validation.Builder().name("publicKey").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).build(),
+ new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectIssuer).build(),
+ new Validation.Builder().name("revocationList").type(ValueType.ID).expectedType(Type.RevocationList).fetch(true).build(),
+ new Validation.Builder().name("id").type(ValueType.ISSUER).messageLevel(MessageLevel.Warning).build(),
+ new Validation.Builder().name("@language").type(ValueType.LANGUAGE).build(),
+ new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).build(),
+ new Validation.Builder().name("related").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Issuer).many(true).fullValidate(false).build(),
+ new Validation.Builder().name("endorsement").type(ValueType.ID).allowRemoteUrl(true).fetch(true).expectedType(Type.Endorsement).many(true).build()
+ ))
+ .put(Type.Profile, List.of(
+ new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.Issuer, Type.Profile)).build(),
+ new Validation.Builder().name("name").type(ValueType.TEXT).required(true).build(),
+ new Validation.Builder().name("description").type(ValueType.TEXT).build(),
+ new Validation.Builder().name("image").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Image).allowDataUri(true).build(),
+ new Validation.Builder().name("url").type(ValueType.URL).required(true).build(),
+ new Validation.Builder().name("email").type(ValueType.EMAIL).required(true).build(),
+ new Validation.Builder().name("telephone").type(ValueType.TELEPHONE).build(),
+ new Validation.Builder().name("publicKey").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).build(),
+ new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectIssuer).build(),
+ new Validation.Builder().name("id").type(ValueType.ISSUER).messageLevel(MessageLevel.Warning).build(),
+ new Validation.Builder().name("@language").type(ValueType.LANGUAGE).build(),
+ new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).build(),
+ new Validation.Builder().name("related").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Profile).many(true).fullValidate(false).build(),
+ new Validation.Builder().name("endorsement").type(ValueType.ID).allowRemoteUrl(true).fetch(true).expectedType(Type.Endorsement).many(true).build() ))
+ .put(Type.RevocationList, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.RevocationList)).build(),
+ new Validation.Builder().name("id").type(ValueType.IRI).build()
+ ))
+ .put(Type.VerificationObject, List.of())
+ .put(Type.VerificationObjectAssertion, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).mustContainOne(List.of("HostedBadge", "SignedBadge")).build(),
+ new Validation.Builder().name("creator").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).build()
+ ))
+ .put(Type.VerificationObjectIssuer, List.of(
+ new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).defaultType(Type.VerificationObject).build(),
+ new Validation.Builder().name("verificationProperty").type(ValueType.COMPACT_IRI).build(),
+ new Validation.Builder().name("startsWith").type(ValueType.URL).many(true).build(),
+ new Validation.Builder().name("allowedOrigins").type(ValueType.URL_AUTHORITY).many(true).build()
+ ))
+ .put(Type.External, Collections.emptyList())
+ .put(Type.Unknown, Collections.emptyList())
+ .build();
+
+ public static final String ID = Assertion.class.getCanonicalName();
+ private static final String ISSUED_ON_PROPERTY_NAME = "issuedOn";
+ private static final String EXPIRES_AT_PROPERTY_NAME = "expires";
+ public static final String JWT_NODE_NAME = ""; // empty because the whole payload is the assertion
+}
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 1b3c501..84e1c9b 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
@@ -1,133 +1,133 @@
package org.oneedtech.inspect.vc;
-import static org.oneedtech.inspect.util.code.Defensives.*;
-import static org.oneedtech.inspect.util.resource.ResourceType.*;
-import static org.oneedtech.inspect.vc.Credential.Type.*;
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
+import static org.oneedtech.inspect.util.resource.ResourceType.JSON;
+import static org.oneedtech.inspect.util.resource.ResourceType.JWT;
+import static org.oneedtech.inspect.util.resource.ResourceType.PNG;
+import static org.oneedtech.inspect.util.resource.ResourceType.SVG;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.oneedtech.inspect.core.probe.GeneratedObject;
-import org.oneedtech.inspect.schema.Catalog;
import org.oneedtech.inspect.schema.SchemaKey;
import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.MoreObjects;
-import com.google.common.collect.ImmutableMap;
+
/**
- * A wrapper object for a verifiable credential. This contains e.g. the origin resource
- * and the extracted JSON data plus any other stuff Probes need.
- * @author mgylling
+ * Base credential class for OB 2.0 Assertions and OB 3.0 and CLR 2.0 Credentials.
+ * This contains e.g. the origin resource and the extracted JSON data.
+ * @author xaracil
*/
-public class Credential extends GeneratedObject {
+public abstract class Credential extends GeneratedObject {
final Resource resource;
final JsonNode jsonData;
- final Credential.Type credentialType;
final String jwt;
-
- public Credential(Resource resource, JsonNode data, String jwt) {
- super(ID, GeneratedObject.Type.INTERNAL);
+ final String issuedOnPropertyName;
+ final String expiresAtPropertyName;
+ final Map schemas;
+
+ protected Credential(String id, Resource resource, JsonNode data, String jwt, Map schemas, String issuedOnPropertyName, String expiresAtPropertyName) {
+ super(id, GeneratedObject.Type.INTERNAL);
this.resource = checkNotNull(resource);
this.jsonData = checkNotNull(data);
this.jwt = jwt; //may be null
-
+ this.schemas = schemas;
+ this.issuedOnPropertyName = issuedOnPropertyName;
+ this.expiresAtPropertyName = expiresAtPropertyName;
+
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;
- }
-
- public JsonNode getJson() {
- return jsonData;
}
- public Credential.Type getCredentialType() {
- return credentialType;
- }
+ public Resource getResource() {
+ return resource;
+ }
- public Optional getJwt() {
+ public JsonNode getJson() {
+ return jsonData;
+ }
+
+ public Optional getJwt() {
return Optional.ofNullable(jwt);
}
-
- public ProofType getProofType() {
- return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL;
- }
-
-
- private static final Map schemas = new ImmutableMap.Builder()
- .put(AchievementCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON)
- .put(ClrCredential, Catalog.CLR_20_CLRCREDENTIAL_JSON)
- .put(VerifiablePresentation, Catalog.CLR_20_CLRCREDENTIAL_JSON)
- .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON)
- .build();
-
- /**
+
+ public String getIssuedOnPropertyName() {
+ return issuedOnPropertyName;
+ }
+
+ public String getExpiresAtPropertyName() {
+ return expiresAtPropertyName;
+ }
+
+ /**
* Get the canonical schema for this credential if such exists.
*/
public Optional getSchemaKey() {
- return Optional.ofNullable(schemas.get(credentialType));
+ return Optional.ofNullable(schemas.get(getCredentialType()));
}
-
- public enum Type {
- AchievementCredential,
- OpenBadgeCredential, //treated as an alias of AchievementCredential
- ClrCredential,
- EndorsementCredential,
- VerifiablePresentation,
- VerifiableCredential, //this is an underspecifier in our context
- Unknown;
-
- public static Credential.Type valueOf (ArrayNode typeArray) {
- if(typeArray != null) {
- Iterator iter = typeArray.iterator();
- while(iter.hasNext()) {
- String value = iter.next().asText();
- if(value.equals("AchievementCredential") || value.equals("OpenBadgeCredential")) {
- return AchievementCredential;
- } else if(value.equals("ClrCredential")) {
- return ClrCredential;
- } else if(value.equals("VerifiablePresentation")) {
- return VerifiablePresentation;
- } else if(value.equals("EndorsementCredential")) {
- return EndorsementCredential;
- }
- }
- }
- return Unknown;
- }
- }
-
- public enum ProofType {
- EXTERNAL,
- EMBEDDED
- }
-
+
+ public abstract CredentialEnum getCredentialType();
+
@Override
public String toString() {
- return MoreObjects.toStringHelper(this)
+ return MoreObjects.toStringHelper(this)
.add("resource", resource.getID())
.add("resourceType", resource.getType())
- .add("credentialType", credentialType)
.add("json", jsonData)
+ .add("jwt", jwt)
.toString();
}
-
- public static final String ID = Credential.class.getCanonicalName();
+
public static final List RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT);
public static final String CREDENTIAL_KEY = "CREDENTIAL_KEY";
-
+
+ public interface CredentialEnum {
+ List getRequiredTypeValues();
+ List getAllowedTypeValues();
+ boolean isAllowedTypeValuesRequired();
+ List getContextUris();
+ String toString();
+ }
+
+ public abstract static class Builder {
+ private Resource resource;
+ private JsonNode jsonData;
+ private String jwt;
+
+ public abstract B build();
+
+ public Builder resource(Resource resource) {
+ this.resource = resource;
+ return this;
+ }
+
+ public Builder jsonData(JsonNode node) {
+ this.jsonData = node;
+ return this;
+ }
+
+ public Builder jwt(String jwt) {
+ this.jwt = jwt;
+ return this;
+ }
+
+ protected Resource getResource() {
+ return resource;
+ }
+
+ protected JsonNode getJsonData() {
+ return jsonData;
+ }
+
+ protected String getJwt() {
+ return jwt;
+ }
+ }
+
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
index 2eb643c..053b08f 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/EndorsementInspector.java
@@ -6,7 +6,7 @@ import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
-import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL;
+import static org.oneedtech.inspect.vc.VerifiableCredential.ProofType.EXTERNAL;
import java.net.URI;
import java.util.ArrayList;
@@ -25,7 +25,7 @@ import org.oneedtech.inspect.util.json.ObjectMapperCache;
import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.UriResource;
import org.oneedtech.inspect.util.resource.context.ResourceContext;
-import org.oneedtech.inspect.vc.Credential.Type;
+import org.oneedtech.inspect.vc.VerifiableCredential.Type;
import org.oneedtech.inspect.vc.probe.ContextPropertyProbe;
import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe;
import org.oneedtech.inspect.vc.probe.ExpirationProbe;
@@ -39,34 +39,35 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
- * An inspector for EndorsementCredential objects.
+ * An inspector for EndorsementCredential objects.
* @author mgylling
*/
public class EndorsementInspector extends VCInspector implements SubInspector {
protected > EndorsementInspector(B builder) {
super(builder);
- }
+ }
@Override
public Report run(Resource resource, Map parentObjects) {
-
+
/*
* The resource param is the top-level credential that embeds the endorsement, we
* expect parentObjects to provide a pointer to the JsonNode we should check.
- *
+ *
* The parent inspector is responsible to decode away possible jwt-ness, so that
- * what we get here is a verbatim json node.
- *
+ * what we get here is a verbatim json node.
+ *
*/
-
- Credential endorsement = (Credential) checkNotNull(parentObjects.get(CREDENTIAL_KEY));
-
+
+ VerifiableCredential endorsement = (VerifiableCredential) checkNotNull(parentObjects.get(CREDENTIAL_KEY));
+
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
-
+
RunContext ctx = new RunContext.Builder()
.put(this)
+ .put(resource)
.put(JACKSON_OBJECTMAPPER, mapper)
.put(JSONPATH_EVALUATOR, jsonPath)
.build();
@@ -74,18 +75,18 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
List accumulator = new ArrayList<>();
int probeCount = 0;
try {
-
+
//context and type properties
- Credential.Type type = Type.EndorsementCredential;
- for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
+ VerifiableCredential.Type type = Type.EndorsementCredential;
+ for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
probeCount++;
accumulator.add(probe.run(endorsement.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
}
-
+
//inline schema (parent inspector has already validated against canonical)
accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx));
-
+
//signatures, proofs
probeCount++;
if(endorsement.getProofType() == EXTERNAL){
@@ -93,16 +94,16 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
accumulator.add(new ExternalProofProbe().run(endorsement, ctx));
} else {
//The credential not contained in a jwt, must have an internal proof.
- accumulator.add(new EmbeddedProofProbe().run(endorsement, ctx));
-
+ accumulator.add(new EmbeddedProofProbe().run(endorsement, ctx));
+
}
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
//check refresh service if we are not already refreshed (check just like in external CLR)
probeCount++;
if(resource.getContext().get(REFRESHED) != TRUE) {
- Optional newID = checkRefreshService(endorsement, ctx);
- if(newID.isPresent()) {
+ Optional newID = checkRefreshService(endorsement, ctx);
+ if(newID.isPresent()) {
//TODO resource.type
return this.run(
new UriResource(new URI(newID.get()))
@@ -111,8 +112,8 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
}
//revocation, expiration and issuance
- for(Probe probe : List.of(new RevocationListProbe(),
- new ExpirationProbe(), new IssuanceProbe())) {
+ for(Probe probe : List.of(new RevocationListProbe(),
+ new ExpirationProbe(), new IssuanceProbe())) {
probeCount++;
accumulator.add(probe.run(endorsement, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
@@ -129,7 +130,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
public Report run(R resource) {
throw new IllegalStateException("must use #run(resource, map)");
}
-
+
public static class Builder extends VCInspector.Builder {
@SuppressWarnings("unchecked")
@Override
@@ -137,5 +138,5 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
return new EndorsementInspector(this);
}
}
-
+
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20EndorsementInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20EndorsementInspector.java
new file mode 100644
index 0000000..5e7d303
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20EndorsementInspector.java
@@ -0,0 +1,150 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.core.probe.RunContext.Key.GENERATED_OBJECT_BUILDER;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.JACKSON_OBJECTMAPPER;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.JSONPATH_EVALUATOR;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.JSON_DOCUMENT_LOADER;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.JWT_CREDENTIAL_NODE_NAME;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.PNG_CREDENTIAL_KEY;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.SVG_CREDENTIAL_QNAME;
+import static org.oneedtech.inspect.core.probe.RunContext.Key.URI_RESOURCE_FACTORY;
+import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.oneedtech.inspect.core.SubInspector;
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.core.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.vc.Assertion.Type;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.payload.PngParser;
+import org.oneedtech.inspect.vc.payload.SvgParser;
+import org.oneedtech.inspect.vc.probe.AssertionRevocationListProbe;
+import org.oneedtech.inspect.vc.probe.ExpirationProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceProbe;
+import org.oneedtech.inspect.vc.probe.VerificationDependenciesProbe;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * An inspector for EndorsementCredential objects.
+ * @author mgylling
+ */
+public class OB20EndorsementInspector extends VCInspector implements SubInspector {
+
+ private DocumentLoader documentLoader;
+ private UriResourceFactory uriResourceFactory;
+
+ protected OB20EndorsementInspector(OB20EndorsementInspector.Builder builder) {
+ super(builder);
+ this.documentLoader = builder.documentLoader;
+ this.uriResourceFactory = builder.uriResourceFactory;
+ }
+
+ @Override
+ public Report run(Resource resource, Map parentObjects) {
+
+ /*
+ * The resource param is the top-level credential that embeds the endorsement, we
+ * expect parentObjects to provide a pointer to the JsonNode we should check.
+ *
+ * The parent inspector is responsible to decode away possible jwt-ness, so that
+ * what we get here is a verbatim json node.
+ *
+ */
+
+ Assertion endorsement = (Assertion) checkNotNull(parentObjects.get(CREDENTIAL_KEY));
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(resource)
+ .put(JACKSON_OBJECTMAPPER, mapper)
+ .put(JSONPATH_EVALUATOR, jsonPath)
+ .put(GENERATED_OBJECT_BUILDER, new Assertion.Builder())
+ .put(PNG_CREDENTIAL_KEY, PngParser.Keys.OB20)
+ .put(SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB20)
+ .put(JSON_DOCUMENT_LOADER, documentLoader)
+ .put(JWT_CREDENTIAL_NODE_NAME, Assertion.JWT_NODE_NAME)
+ .put(URI_RESOURCE_FACTORY, uriResourceFactory)
+ .build();
+
+ parentObjects.entrySet().stream().forEach(entry -> {
+ if (!entry.getKey().equals(CREDENTIAL_KEY)) {
+ ctx.addGeneratedObject(entry.getValue());
+ }
+ });
+
+ List accumulator = new ArrayList<>();
+ int probeCount = 0;
+ try {
+
+ JsonNode endorsementNode = endorsement.getJson();
+ // verification and revocation
+ if (endorsement.getCredentialType() == Type.Endorsement) {
+ for(Probe probe : List.of(new VerificationDependenciesProbe(endorsementNode.get("id").asText(), "claim"),
+ new AssertionRevocationListProbe(endorsementNode.get("id").asText(), "claim"))) {
+ probeCount++;
+ accumulator.add(probe.run(new JsonLdGeneratedObject(endorsementNode.toString()), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+ }
+
+ // expiration and issuance
+ for(Probe probe : List.of(
+ new ExpirationProbe(), new IssuanceProbe())) {
+ probeCount++;
+ accumulator.add(probe.run(endorsement, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ @Override
+ public Report run(R resource) {
+ throw new IllegalStateException("must use #run(resource, map)");
+ }
+
+ public static class Builder extends VCInspector.Builder {
+ private DocumentLoader documentLoader;
+ private UriResourceFactory uriResourceFactory;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public OB20EndorsementInspector build() {
+ return new OB20EndorsementInspector(this);
+ }
+
+ public Builder documentLoader(DocumentLoader documentLoader) {
+ this.documentLoader = documentLoader;
+ return this;
+ }
+
+ public Builder uriResourceFactory(UriResourceFactory uriResourceFactory) {
+ this.uriResourceFactory = uriResourceFactory;
+ return this;
+ }
+
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java
new file mode 100644
index 0000000..57f939a
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java
@@ -0,0 +1,249 @@
+package org.oneedtech.inspect.vc;
+
+import static java.lang.Boolean.TRUE;
+import static java.util.stream.Collectors.toList;
+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.Credential.CREDENTIAL_KEY;
+import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.oneedtech.inspect.core.Inspector;
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+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.JsonPathEvaluator;
+import org.oneedtech.inspect.core.report.Report;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.schema.JsonSchemaCache;
+import org.oneedtech.inspect.util.code.Tuple;
+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.util.resource.UriResource;
+import org.oneedtech.inspect.util.spec.Specification;
+import org.oneedtech.inspect.vc.Assertion.Type;
+import org.oneedtech.inspect.vc.Credential.CredentialEnum;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.ExtensionProbe;
+import org.oneedtech.inspect.vc.jsonld.probe.GraphFetcherProbe;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDValidationProbe;
+import org.oneedtech.inspect.vc.payload.PngParser;
+import org.oneedtech.inspect.vc.payload.SvgParser;
+import org.oneedtech.inspect.vc.probe.AssertionRevocationListProbe;
+import org.oneedtech.inspect.vc.probe.ContextPropertyProbe;
+import org.oneedtech.inspect.vc.probe.CredentialParseProbe;
+import org.oneedtech.inspect.vc.probe.ExpirationProbe;
+import org.oneedtech.inspect.vc.probe.IssuanceProbe;
+import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
+import org.oneedtech.inspect.vc.probe.VerificationDependenciesProbe;
+import org.oneedtech.inspect.vc.probe.VerificationJWTProbe;
+import org.oneedtech.inspect.vc.probe.validation.ValidationPropertyProbeFactory;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A verifier for Open Badges 2.0.
+ * @author xaracil
+ */
+public class OB20Inspector extends VCInspector {
+
+ protected > OB20Inspector(B builder) {
+ super(builder);
+ }
+
+ /* (non-Javadoc)
+ * @see org.oneedtech.inspect.core.Inspector#run(org.oneedtech.inspect.util.resource.Resource)
+ */
+ @Override
+ public Report run(Resource resource) {
+ super.check(resource);
+
+ if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) {
+ JsonSchemaCache.reset();
+ CachingDocumentLoader.reset();
+ }
+
+ ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+ DocumentLoader documentLoader = getDocumentLoader();
+ UriResourceFactory uriResourceFactory = getUriResourceFactory(documentLoader);
+
+ RunContext ctx = new RunContext.Builder()
+ .put(this)
+ .put(resource)
+ .put(Key.JACKSON_OBJECTMAPPER, mapper)
+ .put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .put(Key.GENERATED_OBJECT_BUILDER, new Assertion.Builder())
+ .put(Key.PNG_CREDENTIAL_KEY, PngParser.Keys.OB20)
+ .put(Key.SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB20)
+ .put(Key.JSON_DOCUMENT_LOADER, documentLoader)
+ .put(Key.JWT_CREDENTIAL_NODE_NAME, Assertion.JWT_NODE_NAME)
+ .put(Key.URI_RESOURCE_FACTORY, uriResourceFactory)
+ .build();
+
+ List accumulator = new ArrayList<>();
+ int probeCount = 0;
+
+ try {
+ //detect type (png, svg, json, jwt) and extract json data
+ probeCount++;
+ accumulator.add(new CredentialParseProbe().run(resource, ctx));
+ if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
+
+ // we expect the above to place a generated object in the context
+ Assertion assertion = ctx.getGeneratedObject(resource.getID());
+
+ //context and type properties
+ CredentialEnum type = assertion.getCredentialType();
+ for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
+ probeCount++;
+ accumulator.add(probe.run(assertion.getJson(), ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ // let's compact
+ accumulator.add(new JsonLDCompactionProve(assertion.getCredentialType().getContextUris().get(0)).run(assertion, ctx));
+ if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
+
+ // validate JSON LD
+ JsonLdGeneratedObject jsonLdGeneratedObject = ctx.getGeneratedObject(JsonLDCompactionProve.getId(assertion));
+ accumulator.add(new JsonLDValidationProbe(jsonLdGeneratedObject).run(assertion, ctx));
+ if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
+
+ // validation the Open Badge, from the compacted form
+ JsonNode assertionNode = mapper.readTree(jsonLdGeneratedObject.getJson());
+
+ // mount the graph, flattening embedded resources
+ probeCount++;
+ accumulator.add(new GraphFetcherProbe(assertion).run(assertionNode, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+
+ // perform validations
+ List validations = assertion.getValidations();
+ for (Validation validation : validations) {
+ probeCount++;
+ accumulator.add(ValidationPropertyProbeFactory.of(assertion.getCredentialType().toString(), validation).run(assertionNode, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ // verification and revocation
+ if (assertion.getCredentialType() == Type.Assertion) {
+ for(Probe probe : List.of(new VerificationDependenciesProbe(assertionNode.get("id").asText()),
+ new AssertionRevocationListProbe(assertionNode.get("id").asText()))) {
+ probeCount++;
+ accumulator.add(probe.run(jsonLdGeneratedObject, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ // JWS verification
+ if (assertion.getJwt().isPresent()) {
+ probeCount++;
+ accumulator.add(new VerificationJWTProbe(assertion.getJwt().get()).run(jsonLdGeneratedObject, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+ }
+
+ // expiration and issuance
+ for(Probe probe : List.of(
+ new ExpirationProbe(), new IssuanceProbe())) {
+ probeCount++;
+ accumulator.add(probe.run(assertion, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ // get all json-ld generated objects for both extension and endorsements validation
+ List jsonLdGeneratedObjects = ctx.getGeneratedObjects().values().stream()
+ .filter(generatedObject -> generatedObject instanceof JsonLdGeneratedObject)
+ .map(obj -> {
+
+ try {
+ return mapper.readTree(((JsonLdGeneratedObject) obj).getJson());
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Couldn't not parse " + obj.getId() + ": contains invalid JSON");
+ }
+ })
+ .collect(toList());
+
+ // validate extensions
+ List> extensionProbeTuples = jsonLdGeneratedObjects.stream()
+ .flatMap(node -> getExtensionProbes(node, "id").stream())
+ .collect(toList());
+ for (Tuple extensionProbeTuple : extensionProbeTuples) {
+ probeCount++;
+ accumulator.add(extensionProbeTuple.t1.run(extensionProbeTuple.t2, ctx));
+ if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
+ }
+
+ // Embedded endorsements. Pass document loader because it has already cached documents, and it has localdomains for testing
+ OB20EndorsementInspector endorsementInspector = new OB20EndorsementInspector.Builder()
+ .documentLoader(documentLoader)
+ .uriResourceFactory(uriResourceFactory)
+ .build();
+
+ // get endorsements for all JSON_LD objects in the graph
+ List endorsements = jsonLdGeneratedObjects.stream().flatMap(node -> {
+ // return endorsement node, filtering out the on inside @context
+ return asNodeList(node, "$..endorsement", jsonPath).stream().filter(endorsementNode -> !endorsementNode.isObject());
+ })
+ .collect(toList());
+
+ for(JsonNode node : endorsements) {
+ probeCount++;
+ // get endorsement json from context
+ UriResource uriResource = uriResourceFactory.of(node.asText());
+ JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
+ if (resolved == null) {
+ throw new IllegalArgumentException("endorsement " + node.toString() + " not found in graph");
+ }
+
+ Assertion endorsement = new Assertion.Builder().resource(resource).jsonData(mapper.readTree(resolved.getJson())).build();
+ // pass graph to subinspector
+ Map parentObjects = new HashMap<>(ctx.getGeneratedObjects());
+ parentObjects.put(CREDENTIAL_KEY, endorsement);
+ accumulator.add(endorsementInspector.run(resource, parentObjects));
+ }
+
+ } catch (Exception e) {
+ accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
+ }
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+ }
+
+ public static class Builder extends VCInspector.Builder {
+
+ public Builder() {
+ super();
+ // don't allow local redirections by default
+ super.behaviors.put(Behavior.ALLOW_LOCAL_REDIRECTION, false);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public OB20Inspector build() {
+ set(Specification.OB20);
+ set(ResourceType.OPENBADGE);
+ return new OB20Inspector(this);
+ }
+ }
+
+ public static class Behavior extends Inspector.Behavior {
+ /**
+ * Whether to support local redirection of uris
+ */
+ public static final String ALLOW_LOCAL_REDIRECTION = "ALLOW_LOCAL_REDIRECTION";
+ }
+}
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 96e4236..163b9ec 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,7 +6,7 @@ import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
import static org.oneedtech.inspect.util.code.Defensives.*;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
import static org.oneedtech.inspect.vc.Credential.CREDENTIAL_KEY;
-import static org.oneedtech.inspect.vc.Credential.ProofType.EXTERNAL;
+import static org.oneedtech.inspect.vc.VerifiableCredential.ProofType.EXTERNAL;
import static org.oneedtech.inspect.vc.payload.PayloadParser.fromJwt;
import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList;
@@ -34,7 +34,9 @@ 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.Credential.Type;
+import org.oneedtech.inspect.vc.VerifiableCredential.Type;
+import org.oneedtech.inspect.vc.payload.PngParser;
+import org.oneedtech.inspect.vc.payload.SvgParser;
import org.oneedtech.inspect.vc.probe.ContextPropertyProbe;
import org.oneedtech.inspect.vc.probe.CredentialParseProbe;
import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe;
@@ -56,112 +58,120 @@ import com.google.common.collect.ImmutableList;
* @author mgylling
*/
public class OB30Inspector extends VCInspector implements SubInspector {
- protected final List> userProbes;
-
- protected OB30Inspector(OB30Inspector.Builder builder) {
- super(builder);
- this.userProbes = ImmutableList.copyOf(builder.probes);
+ protected final List> userProbes;
+
+ protected OB30Inspector(OB30Inspector.Builder builder) {
+ super(builder);
+ this.userProbes = ImmutableList.copyOf(builder.probes);
}
-
+
//https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit#
//https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#verificaton-and-validation
-
+
/*
* This inspector supports both standalone openbadge verification, as well as verification of
- * AchievementCredentials embedded in e.g. CLR.
- *
+ * AchievementCredentials embedded in e.g. CLR.
+ *
* When verifying a standalone AchievementCredential, call the run(Resource) method. When verifying
- * an embedded AchievementCredential, call the run(Resource, Map) method.
+ * an embedded AchievementCredential, call the run(Resource, Map) method.
*/
-
+
@Override
- public Report run(Resource resource) {
+ public Report run(Resource resource) {
super.check(resource); //TODO because URIs, this should be a fetch and cache
-
+
if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) {
JsonSchemaCache.reset();
CachingDocumentLoader.reset();
}
-
+
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
-
+
RunContext ctx = new RunContext.Builder()
.put(this)
.put(resource)
.put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath)
- .build();
-
+ .put(Key.GENERATED_OBJECT_BUILDER, new VerifiableCredential.Builder())
+ .put(Key.PNG_CREDENTIAL_KEY, PngParser.Keys.OB30)
+ .put(Key.SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB30)
+ .put(Key.JWT_CREDENTIAL_NODE_NAME, VerifiableCredential.JWT_NODE_NAME)
+ .build();
+
List accumulator = new ArrayList<>();
int probeCount = 0;
-
- try {
+
+ try {
//detect type (png, svg, json, jwt) and extract json data
probeCount++;
- accumulator.add(new CredentialParseProbe().run(resource, ctx));
+ accumulator.add(new CredentialParseProbe().run(resource, ctx));
if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
-
- //we expect the above to place a generated object in the context
- Credential ob = ctx.getGeneratedObject(Credential.ID);
-
+
+ //we expect the above to place a generated object in the context
+ VerifiableCredential ob = ctx.getGeneratedObject(VerifiableCredential.ID);
+
//call the subinspector method of this
- Report subReport = this.run(resource, Map.of(Credential.CREDENTIAL_KEY, ob));
+ Report subReport = this.run(resource, Map.of(VerifiableCredential.CREDENTIAL_KEY, ob));
probeCount += subReport.getSummary().getTotalRun();
accumulator.add(subReport);
-
+
//finally, run any user-added probes
- for(Probe probe : userProbes) {
+ for(Probe probe : userProbes) {
probeCount++;
accumulator.add(probe.run(ob, ctx));
}
-
+
} catch (Exception e) {
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
}
-
- return new Report(ctx, new ReportItems(accumulator), probeCount);
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
}
-
+
@Override
public Report run(Resource resource, Map parentObjects) {
-
- Credential ob = checkNotNull((Credential)parentObjects.get(CREDENTIAL_KEY));
-
+
+ VerifiableCredential ob = checkNotNull((VerifiableCredential)parentObjects.get(CREDENTIAL_KEY));
+
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
+ VerifiableCredential.Builder credentialBuilder = new VerifiableCredential.Builder();
RunContext ctx = new RunContext.Builder()
.put(this)
.put(resource)
.put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .put(Key.GENERATED_OBJECT_BUILDER, credentialBuilder)
+ .put(Key.PNG_CREDENTIAL_KEY, PngParser.Keys.OB30)
+ .put(Key.SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB30)
.build();
-
+
List accumulator = new ArrayList<>();
int probeCount = 0;
-
+
try {
-
+
//context and type properties
- Credential.Type type = Type.OpenBadgeCredential;
- for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
+ VerifiableCredential.Type type = Type.OpenBadgeCredential;
+ for(Probe probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
probeCount++;
accumulator.add(probe.run(ob.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
}
-
+
//canonical schema and inline schemata
SchemaKey schema = ob.getSchemaKey().orElseThrow();
- for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) {
+ for(Probe probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) {
probeCount++;
accumulator.add(probe.run(ob.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
}
-
- //credentialSubject
+
+ //credentialSubject
probeCount++;
accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx));
-
+
//signatures, proofs
probeCount++;
if(ob.getProofType() == EXTERNAL){
@@ -169,57 +179,57 @@ public class OB30Inspector extends VCInspector implements SubInspector {
accumulator.add(new ExternalProofProbe().run(ob, ctx));
} else {
//The credential not contained in a jwt, must have an internal proof.
- accumulator.add(new EmbeddedProofProbe().run(ob, ctx));
+ accumulator.add(new EmbeddedProofProbe().run(ob, ctx));
}
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
-
+
//check refresh service if we are not already refreshed
probeCount++;
if(resource.getContext().get(REFRESHED) != TRUE) {
- Optional newID = checkRefreshService(ob, ctx);
- if(newID.isPresent()) {
+ Optional newID = checkRefreshService(ob, ctx);
+ if(newID.isPresent()) {
return this.run(
new UriResource(new URI(newID.get()))
.setContext(new ResourceContext(REFRESHED, TRUE)));
}
}
-
+
//revocation, expiration and issuance
- for(Probe probe : List.of(new RevocationListProbe(),
- new ExpirationProbe(), new IssuanceProbe())) {
+ for(Probe probe : List.of(new RevocationListProbe(),
+ new ExpirationProbe(), new IssuanceProbe())) {
probeCount++;
accumulator.add(probe.run(ob, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
}
-
- //embedded endorsements
- EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build();
-
+
+ //embedded endorsements
+ EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build();
+
List endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath);
for(JsonNode node : endorsements) {
probeCount++;
- Credential endorsement = new Credential(resource, node);
+ VerifiableCredential endorsement = credentialBuilder.resource(resource).jsonData(node).build();
accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
- }
-
- //embedded jwt endorsements
+ }
+
+ //embedded jwt endorsements
endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath);
for(JsonNode node : endorsements) {
probeCount++;
String jwt = node.asText();
JsonNode vcNode = fromJwt(jwt, ctx);
- Credential endorsement = new Credential(resource, vcNode, jwt);
+ VerifiableCredential endorsement = credentialBuilder.resource(resource).jsonData(node).jwt(jwt).build();
accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
}
-
+
} catch (Exception e) {
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
}
-
- return new Report(ctx, new ReportItems(accumulator), probeCount);
-
+
+ return new Report(ctx, new ReportItems(accumulator), probeCount);
+
}
-
+
public static class Builder extends VCInspector.Builder {
@SuppressWarnings("unchecked")
@Override
@@ -228,5 +238,5 @@ public class OB30Inspector extends VCInspector implements SubInspector {
set(ResourceType.OPENBADGE);
return new OB30Inspector(this);
}
- }
+ }
}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
index a4a90dc..60dd4ac 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VCInspector.java
@@ -1,8 +1,16 @@
package org.oneedtech.inspect.vc;
+import static java.util.stream.Collectors.toList;
+import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asStringList;
+
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.oneedtech.inspect.core.Inspector;
import org.oneedtech.inspect.core.probe.Outcome;
@@ -10,7 +18,14 @@ import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.Report;
import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.util.code.Tuple;
+import org.oneedtech.inspect.vc.jsonld.probe.ExtensionProbe;
+import org.oneedtech.inspect.vc.resource.DefaultUriResourceFactory;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+import com.apicatalog.jsonld.loader.DocumentLoader;
import com.fasterxml.jackson.databind.JsonNode;
/**
@@ -18,19 +33,19 @@ import com.fasterxml.jackson.databind.JsonNode;
* @author mgylling
*/
public abstract class VCInspector extends Inspector {
-
+
protected > VCInspector(B builder) {
- super(builder);
+ super(builder);
}
-
+
protected Report abort(RunContext ctx, List accumulator, int probeCount) {
return new Report(ctx, new ReportItems(accumulator), probeCount);
}
-
+
protected boolean broken(List accumulator) {
return broken(accumulator, false);
}
-
+
protected boolean broken(List accumulator, boolean force) {
if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) {
return false;
@@ -40,15 +55,15 @@ public abstract class VCInspector extends Inspector {
}
return false;
}
-
+
/**
- * If the AchievementCredential or EndorsementCredential has a “refreshService” property and the type of the
- * RefreshService object is “1EdTechCredentialRefresh”, you should fetch the refreshed credential from the URL
- * provided, then start the verification process over using the response as input. If the request fails,
+ * If the AchievementCredential or EndorsementCredential has a “refreshService” property and the type of the
+ * RefreshService object is “1EdTechCredentialRefresh”, you should fetch the refreshed credential from the URL
+ * provided, then start the verification process over using the response as input. If the request fails,
* the credential is invalid.
*/
- protected Optional checkRefreshService(Credential crd, RunContext ctx) {
- JsonNode refreshServiceNode = crd.getJson().get("refreshService");
+ protected Optional checkRefreshService(VerifiableCredential crd, RunContext ctx) {
+ JsonNode refreshServiceNode = crd.getJson().get("refreshService");
if(refreshServiceNode != null) {
JsonNode serviceTypeNode = refreshServiceNode.get("type");
if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) {
@@ -56,22 +71,74 @@ public abstract class VCInspector extends Inspector {
if(serviceURINode != null) {
return Optional.of(serviceURINode.asText());
}
- }
- }
+ }
+ }
return Optional.empty();
}
+ /**
+ * Creates a caching document loader for loading json resources
+ * @return document loader for loading json resources
+ */
+ protected DocumentLoader getDocumentLoader() {
+ return new CachingDocumentLoader();
+ }
+
+ protected UriResourceFactory getUriResourceFactory(DocumentLoader documentLoader) {
+ return new DefaultUriResourceFactory();
+ }
+
+ protected List> getExtensionProbes(JsonNode node, String entryPath) {
+ List> probes = new ArrayList<>();
+ if (!node.isObject()) {
+ return probes;
+ }
+
+ if (node.has("type")) {
+ List types = asStringList(node.get("type"));
+
+ // only validate extension types
+ if (types.contains("Extension")) {
+ List typesToTest = types.stream().filter(type -> !type.equals("Extension")).collect(toList());
+ // add an extension Probe
+ probes.add(new Tuple(new ExtensionProbe(entryPath, typesToTest), node));
+ }
+ }
+
+
+ probes.addAll(StreamSupport
+ .stream(Spliterators.spliteratorUnknownSize(node.fields(), 0), false)
+ .filter(e -> !e.getKey().equals("id") && !e.getKey().equals("type"))
+ .flatMap(entry -> {
+ if (entry.getValue().isArray()) {
+ // recursive call
+ List childNodes = JsonNodeUtil.asNodeList(entry.getValue());
+ List> subProbes = new ArrayList<>();
+ for (int i = 0; i < childNodes.size(); i++) {
+ JsonNode childNode = childNodes.get(i);
+ subProbes.addAll(getExtensionProbes(childNode, entryPath + "." + entry.getKey() + "[" + i + "]"));
+ }
+ return subProbes.stream();
+ } else {
+ return getExtensionProbes(entry.getValue(), entryPath + "." + entry.getKey()).stream();
+ }
+ })
+ .collect(Collectors.toList())
+ );
+ return probes;
+ }
+
protected static final String REFRESHED = "is.refreshed.credential";
-
+
public abstract static class Builder> extends Inspector.Builder {
- final List> probes;
+ final List> probes;
public Builder() {
super();
this.probes = new ArrayList<>();
}
-
- public VCInspector.Builder add(Probe probe) {
+
+ public VCInspector.Builder add(Probe probe) {
probes.add(probe);
return this;
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java
new file mode 100644
index 0000000..7815376
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java
@@ -0,0 +1,221 @@
+package org.oneedtech.inspect.vc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Validation class for Open Badges 2.0 types
+ */
+public class Validation {
+ private final String name;
+ private final Assertion.ValueType type;
+ private final boolean required;
+ private final boolean many;
+ private final List mustContainOne;
+ private final List prerequisites;
+ private final List expectedTypes;
+ private final boolean allowRemoteUrl;
+ private final boolean allowDataUri;
+ private final boolean fetch;
+ private final String defaultType;
+ private final boolean fullValidate;
+ private MessageLevel messageLevel;
+ private final boolean allowFlattenEmbeddedResource;
+
+ public Validation(Builder builder) {
+ this.name = builder.name;
+ this.type = builder.type;
+ this.required = builder.required;
+ this.many = builder.many;
+ this.mustContainOne = builder.mustContainOne;
+ this.prerequisites = builder.prerequisites;
+ this.expectedTypes = builder.expectedTypes;
+ this.allowRemoteUrl = builder.allowRemoteUrl;
+ this.allowDataUri = builder.allowDataUri;
+ this.fetch = builder.fetch;
+ this.defaultType = builder.defaultType;
+ this.fullValidate = builder.fullValidate;
+ this.messageLevel = builder.messageLevel;
+ this.allowFlattenEmbeddedResource = builder.allowFlattenEmbeddedResource;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Assertion.ValueType getType() {
+ return type;
+ }
+
+ public boolean isRequired() {
+ return required;
+ }
+
+ public boolean isMany() {
+ return many;
+ }
+
+ public List getMustContainOne() {
+ return mustContainOne;
+ }
+
+ public List getPrerequisites() {
+ return prerequisites;
+ }
+
+ public List getExpectedTypes() {
+ return expectedTypes;
+ }
+
+ public boolean isAllowRemoteUrl() {
+ return allowRemoteUrl;
+ }
+
+ public boolean isAllowDataUri() {
+ return allowDataUri;
+ }
+
+ public boolean isFetch() {
+ return fetch;
+ }
+
+ public String getDefaultType() {
+ return defaultType;
+ }
+
+ public boolean isFullValidate() {
+ return fullValidate;
+ }
+
+ public MessageLevel getMessageLevel() {
+ return messageLevel;
+ }
+
+ public boolean isAllowFlattenEmbeddedResource() {
+ return allowFlattenEmbeddedResource;
+ }
+
+ public enum MessageLevel {
+ Warning,
+ Error
+ }
+
+ public static class Builder {
+ private String name;
+ private Assertion.ValueType type;
+ private boolean required;
+ private boolean many;
+ private List mustContainOne;
+ private List prerequisites;
+ private List expectedTypes;
+ private boolean allowRemoteUrl;
+ private boolean allowDataUri;
+ private boolean fetch;
+ private String defaultType;
+ private boolean fullValidate;
+ private MessageLevel messageLevel;
+ private boolean allowFlattenEmbeddedResource;
+
+ public Builder() {
+ this.mustContainOne = new ArrayList<>();
+ this.prerequisites = new ArrayList<>();
+ this.expectedTypes = new ArrayList<>();
+ this.messageLevel = MessageLevel.Error;
+ this.fullValidate = true; // by default, full validation
+ }
+
+ public Builder name(String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder type(Assertion.ValueType type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder required(boolean required) {
+ this.required = required;
+ return this;
+ }
+
+ public Builder many(boolean many) {
+ this.many = many;
+ return this;
+ }
+
+ public Builder mustContainOne(List elems) {
+ this.mustContainOne = elems;
+ return this;
+ }
+
+ public Builder mustContainOneType(List types) {
+ this.mustContainOne = types.stream().map(Assertion.Type::toString).collect(Collectors.toList());
+ return this;
+ }
+
+ public Builder prerequisites(List elems) {
+ this.prerequisites = elems;
+ return this;
+ }
+
+ public Builder prerequisite(Validation elem) {
+ this.prerequisites = List.of(elem);
+ return this;
+ }
+
+ public Builder expectedTypes(List elems) {
+ this.expectedTypes = elems;
+ return this;
+ }
+
+ public Builder expectedType(Assertion.Type type) {
+ this.expectedTypes = List.of(type);
+ return this;
+ }
+
+ public Builder allowRemoteUrl(boolean allowRemoteUrl) {
+ this.allowRemoteUrl = allowRemoteUrl;
+ return this;
+ }
+
+ public Builder allowDataUri(boolean allowDataUri) {
+ this.allowDataUri = allowDataUri;
+ return this;
+ }
+
+ public Builder fetch(boolean fetch) {
+ this.fetch = fetch;
+ return this;
+ }
+
+ public Builder defaultType(Assertion.Type defaultType) {
+ return defaultType(defaultType.toString());
+ }
+
+ public Builder defaultType(String defaultType) {
+ this.defaultType = defaultType;
+ return this;
+ }
+
+ public Builder fullValidate(boolean fullValidate) {
+ this.fullValidate = fullValidate;
+ return this;
+ }
+
+ public Builder messageLevel(MessageLevel messageLevel) {
+ this.messageLevel = messageLevel;
+ return this;
+ }
+
+ public Builder allowFlattenEmbeddedResource(boolean allowFlattenEmbeddedResource) {
+ this.allowFlattenEmbeddedResource = allowFlattenEmbeddedResource;
+ return this;
+ }
+
+ public Validation build() {
+ return new Validation(this);
+ }
+ }
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java
new file mode 100644
index 0000000..2b29f9f
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/VerifiableCredential.java
@@ -0,0 +1,148 @@
+package org.oneedtech.inspect.vc;
+
+import static org.oneedtech.inspect.vc.VerifiableCredential.Type.AchievementCredential;
+import static org.oneedtech.inspect.vc.VerifiableCredential.Type.ClrCredential;
+import static org.oneedtech.inspect.vc.VerifiableCredential.Type.EndorsementCredential;
+import static org.oneedtech.inspect.vc.VerifiableCredential.Type.VerifiablePresentation;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.oneedtech.inspect.schema.Catalog;
+import org.oneedtech.inspect.schema.SchemaKey;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * A wrapper object for a verifiable credential. This contains e.g. the origin resource
+ * and the extracted JSON data plus any other stuff Probes need.
+ * @author mgylling
+ */
+public class VerifiableCredential extends Credential {
+ final VerifiableCredential.Type credentialType;
+
+ protected VerifiableCredential(Resource resource, JsonNode data, String jwt, Map schemas, String issuedOnPropertyName, String expiresAtPropertyName) {
+ super(ID, resource, data, jwt, schemas, issuedOnPropertyName, expiresAtPropertyName);
+
+ JsonNode typeNode = jsonData.get("type");
+ this.credentialType = VerifiableCredential.Type.valueOf(typeNode);
+ }
+
+ public CredentialEnum getCredentialType() {
+ return credentialType;
+ }
+
+ public ProofType getProofType() {
+ return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL;
+ }
+
+ private static final Map schemas = new ImmutableMap.Builder()
+ .put(AchievementCredential, Catalog.OB_30_ACHIEVEMENTCREDENTIAL_JSON)
+ .put(ClrCredential, Catalog.CLR_20_CLRCREDENTIAL_JSON)
+ .put(VerifiablePresentation, Catalog.CLR_20_CLRCREDENTIAL_JSON)
+ .put(EndorsementCredential, Catalog.OB_30_ENDORSEMENTCREDENTIAL_JSON)
+ .build();
+
+ private static final Map, List> contextMap = new ImmutableMap.Builder, List>()
+ .put(Set.of(Type.OpenBadgeCredential, AchievementCredential, EndorsementCredential),
+ List.of("https://www.w3.org/2018/credentials/v1",
+ //"https://purl.imsglobal.org/spec/ob/v3p0/context.json")) //dev legacy
+ "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
+ .put(Set.of(ClrCredential),
+ List.of("https://www.w3.org/2018/credentials/v1",
+ // "https://dc.imsglobal.org/draft/clr/v2p0/context", //dev legacy
+ // "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) //dev legacy
+ "https://purl.imsglobal.org/spec/clr/v2p0/context.json",
+ "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
+
+ .build();
+
+ public enum Type implements CredentialEnum {
+ AchievementCredential(Collections.emptyList()),
+ OpenBadgeCredential(List.of("OpenBadgeCredential", "AchievementCredential")), //treated as an alias of AchievementCredential
+ ClrCredential(List.of("ClrCredential")),
+ EndorsementCredential(List.of("EndorsementCredential")),
+ VerifiablePresentation(Collections.emptyList()),
+ VerifiableCredential(List.of("VerifiableCredential")), //this is an underspecifier in our context
+ Unknown(Collections.emptyList());
+
+ private final List allowedTypeValues;
+
+ Type(List allowedTypeValues) {
+ this.allowedTypeValues = allowedTypeValues;
+ }
+
+ public static VerifiableCredential.Type valueOf (JsonNode typeNode) {
+ if(typeNode != null) {
+ List values = JsonNodeUtil.asStringList(typeNode);
+ for (String value : values) {
+ if(value.equals("AchievementCredential") || value.equals("OpenBadgeCredential")) {
+ return AchievementCredential;
+ } else if(value.equals("ClrCredential")) {
+ return ClrCredential;
+ } else if(value.equals("VerifiablePresentation")) {
+ return VerifiablePresentation;
+ } else if(value.equals("EndorsementCredential")) {
+ return EndorsementCredential;
+ }
+ }
+ }
+ return Unknown;
+ }
+
+ @Override
+ public List getRequiredTypeValues() {
+ return List.of("VerifiableCredential");
+ }
+
+ @Override
+ public List getAllowedTypeValues() {
+ return allowedTypeValues;
+ }
+
+ @Override
+ public boolean isAllowedTypeValuesRequired() {
+ return true;
+ }
+ @Override
+ public List getContextUris() {
+ return contextMap.get(contextMap.keySet()
+ .stream()
+ .filter(s->s.contains(this))
+ .findFirst()
+ .orElseThrow(()-> new IllegalArgumentException(this.name() + " not recognized")));
+ }
+ }
+
+ public enum ProofType {
+ EXTERNAL,
+ EMBEDDED
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("super", super.toString())
+ .add("credentialType", credentialType)
+ .toString();
+ }
+
+ public static class Builder extends Credential.Builder {
+ @Override
+ public VerifiableCredential build() {
+ return new VerifiableCredential(getResource(), getJsonData(), getJwt(), schemas, ISSUED_ON_PROPERTY_NAME, EXPIRES_AT_PROPERTY_NAME);
+ }
+ }
+
+ public static final String ID = VerifiableCredential.class.getCanonicalName();
+ private static final String ISSUED_ON_PROPERTY_NAME = "issuanceDate";
+ private static final String EXPIRES_AT_PROPERTY_NAME = "expirationDate";
+ public static final String JWT_NODE_NAME = "vc";
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java
new file mode 100644
index 0000000..c53662c
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java
@@ -0,0 +1,31 @@
+package org.oneedtech.inspect.vc.jsonld;
+
+import org.oneedtech.inspect.core.probe.GeneratedObject;
+
+public class JsonLdGeneratedObject extends GeneratedObject {
+ private String json;
+
+ public JsonLdGeneratedObject(String json) {
+ this(ID, json);
+ }
+
+ public JsonLdGeneratedObject(String id, String json) {
+ super(id, GeneratedObject.Type.INTERNAL);
+ this.json = json;
+ }
+
+ public String getJson() {
+ return json;
+ }
+
+ /**
+ * Update internal json. We allow this update because some validations updates JSON-LD id attributes with
+ * autogenerated ones
+ * @param json
+ */
+ public void setJson(String json) {
+ this.json = json;
+ }
+
+ public static final String ID = JsonLdGeneratedObject.class.getCanonicalName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/ExtensionProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/ExtensionProbe.java
new file mode 100644
index 0000000..c1be1ef
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/ExtensionProbe.java
@@ -0,0 +1,169 @@
+package org.oneedtech.inspect.vc.jsonld.probe;
+
+import static java.util.stream.Collectors.joining;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+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.JsonSchemaProbe;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.apicatalog.jsonld.JsonLd;
+import com.apicatalog.jsonld.JsonLdError;
+import com.apicatalog.jsonld.JsonLdOptions;
+import com.apicatalog.jsonld.document.Document;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.apicatalog.jsonld.loader.DocumentLoader;
+import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.util.TokenBuffer;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion.VersionFlag;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
+
+/**
+ * Probe for extensions in OB 2.0
+ * Maps to task "VALIDATE_EXTENSION_NODE" in python implementation
+ * @author xaracil
+ */
+public class ExtensionProbe extends Probe {
+ private final List typesToTest;
+
+ public ExtensionProbe(String entryPath, List typesToTest) {
+ super(ID, entryPath, typesToTest.stream().collect(joining()));
+ this.typesToTest = typesToTest;
+ }
+
+ @Override
+ public ReportItems run(JsonNode node, RunContext ctx) throws Exception {
+ ReportItems reportItems = new ReportItems();
+ DocumentLoader documentLoader = (DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER);
+ Set contexts = null;
+ if (documentLoader instanceof CachingDocumentLoader) {
+ contexts = new HashSet<>(((CachingDocumentLoader) documentLoader).getContexts());
+ } else {
+ contexts = Set.of();
+ }
+
+ // compact contexts
+ URI ob20contextUri = new URI(CONTEXT_URI_STRING);
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ for (URI uri : contexts) {
+ if (!uri.equals(ob20contextUri)) {
+ JsonLdOptions options = new JsonLdOptions(documentLoader);
+ Document contextDocument = documentLoader.loadDocument(uri, new DocumentLoaderOptions());
+ JsonNode contextJson = mapper.readTree(contextDocument.getJsonContent().orElseThrow().toString());
+
+ JsonObject compactedContext = JsonLd.compact(uri, "https://w3id.org/openbadges/v2")
+ .options(options)
+ .get();
+ JsonNode context = mapper.readTree(compactedContext.toString());
+ List validations = JsonNodeUtil.asNodeList(context.get("validation"));
+ for (JsonNode validation : validations) {
+ if (isLdTermInList(validation.get("validatesType"), options)) {
+ JsonNode schemaJson = null;
+ URI schemaUri = null;
+ try {
+ schemaUri = new URI(validation.get("validationSchema").asText().strip());
+ // check schema is valid
+ Document schemaDocument = documentLoader.loadDocument(schemaUri, new DocumentLoaderOptions());
+ schemaJson = mapper.readTree(schemaDocument.getJsonContent().orElseThrow().toString());
+ } catch (Exception e) {
+ return fatal("Could not load JSON-schema from URL " + schemaUri, ctx);
+ }
+
+ reportItems = new ReportItems(List.of(reportItems, validateSingleExtension(node, uri, contextJson, validation.get("validatesType").asText().strip(), schemaJson, schemaUri, options, ctx)));
+ }
+ }
+ }
+ }
+
+ if (reportItems.size() == 0) {
+ return error("Could not determine extension type to test", ctx);
+ }
+
+ return reportItems;
+ }
+
+ private boolean isLdTermInList(JsonNode termNode, JsonLdOptions options) throws JsonLdError {
+ JsonDocument jsonDocument = JsonDocument.of(Json.createObjectBuilder()
+ .add("@context", CONTEXT_URI_STRING)
+ .add("_:term", Json.createObjectBuilder()
+ .add("@type", termNode.asText().strip()))
+ .add("_:list", Json.createObjectBuilder()
+ .add("@type", Json.createArrayBuilder(typesToTest)))
+ .build());
+ JsonArray expandedDocument = JsonLd.expand(jsonDocument)
+ .options(options)
+ .get();
+
+ JsonArray list = expandedDocument.getJsonObject(0).getJsonArray("_:list").getJsonObject(0).getJsonArray("@type");
+ JsonValue term = expandedDocument.getJsonObject(0).getJsonArray("_:term").getJsonObject(0).getJsonArray("@type").get(0);
+
+ return list.contains(term);
+ }
+
+ private ReportItems validateSingleExtension(JsonNode node, URI uri, JsonNode context, String string, JsonNode schemaJson, URI schemaUri, JsonLdOptions options, RunContext ctx) throws JsonGenerationException, JsonMappingException, IOException, JsonLdError {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+
+ // validate against JSON schema, using a copy of the node
+ TokenBuffer tb = new TokenBuffer(mapper, false);
+ mapper.writeValue(tb, node);
+ JsonNode auxNode = mapper.readTree(tb.asParser());
+ ObjectReader readerForUpdating = mapper.readerForUpdating(auxNode);
+ JsonNode merged = readerForUpdating.readValue("{\"@context\": \"" + CONTEXT_URI_STRING + "\"}");
+
+ // combine contexts
+ JsonDocument contextsDocument = combineContexts(context);
+
+ JsonObject compactedObject = JsonLd.compact(JsonDocument.of(new StringReader(merged.toString())), contextsDocument)
+ .options(options)
+ .get();
+
+ // schema probe on compactedObject and schema
+ JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V4);
+ JsonSchema schema = factory.getSchema(schemaUri, schemaJson);
+ return new JsonSchemaProbe(schema).run(mapper.readTree(compactedObject.toString()), ctx);
+ }
+
+ private JsonDocument combineContexts(JsonNode context) {
+ List contexts = JsonNodeUtil.asNodeList(context);
+ JsonArrayBuilder contextArrayBuilder = Json.createArrayBuilder();
+ contextArrayBuilder.add(CONTEXT_URI_STRING); // add OB context to the list
+ for (JsonNode contextNode : contexts) {
+ if (contextNode.isTextual()) {
+ contextArrayBuilder.add(contextNode.asText().strip());
+ } else if (contextNode.isObject() && contextNode.hasNonNull("@context")) {
+ contextArrayBuilder.add(Json.createReader(new StringReader(contextNode.get("@context").toString())).readObject());
+
+ }
+ }
+
+ JsonDocument contextsDocument = JsonDocument.of(Json.createObjectBuilder()
+ .add("@context", contextArrayBuilder.build())
+ .build());
+ return contextsDocument;
+ }
+
+ public static final String ID = ExtensionProbe.class.getSimpleName();
+ private static final String CONTEXT_URI_STRING = "https://w3id.org/openbadges/v2";
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java
new file mode 100644
index 0000000..b5958e1
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java
@@ -0,0 +1,209 @@
+package org.oneedtech.inspect.vc.jsonld.probe;
+
+import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI_OR_URL;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.List;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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.report.ReportItems;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.Assertion;
+import org.oneedtech.inspect.vc.Assertion.Type;
+import org.oneedtech.inspect.vc.Assertion.ValueType;
+import org.oneedtech.inspect.vc.Validation;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.probe.CredentialParseProbe;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.google.common.io.Resources;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+/**
+ * Probe for fetching all elements in the graph for Open Badges 2.0 validation
+ * Contains the fetch part of "VALIDATE_TYPE_PROPERTY" task in python implementation, as well as the "FLATTEN_EMBEDDED_RESOURCE" task
+ * @author xaracil
+ */
+public class GraphFetcherProbe extends Probe {
+ private final Assertion assertion;
+
+ public GraphFetcherProbe(Assertion assertion) {
+ super(ID);
+ this.assertion = assertion;
+ }
+
+ @Override
+ public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+ ReportItems result = new ReportItems();
+
+ // get validations of IDs and fetch
+ List validations = assertion.getValidations().stream()
+ .filter(validation -> validation.getType() == ValueType.ID && validation.isFetch())
+ .collect(Collectors.toList());
+
+ for (Validation validation : validations) {
+ JsonNode node = root.get(validation.getName());
+
+ if (node == null) {
+ // if node is null, continue. ValidationPropertyProbe will check if the field was required
+ continue;
+ }
+
+ // flatten embeded resource
+ if (validation.isAllowFlattenEmbeddedResource()) {
+ if (!node.isTextual()) {
+ if (!node.isObject()) {
+ return error("Property " + validation.getName() + " referenced from " + assertion.getJson().toString() + " is not a JSON object or string as expected", ctx);
+ }
+
+ JsonNode idNode = node.get("id");
+ if (idNode == null) {
+ // add a new node to the graph
+ UUID newId = UUID.randomUUID();
+ JsonNode merged = createNewJson(ctx, "{\"id\": \"_:" + newId + "\"}");
+ ctx.addGeneratedObject(new JsonLdGeneratedObject(JsonLDCompactionProve.getId(newId.toString()), merged.toString()));
+
+ // update existing node with new id
+ updateNode(validation, idNode, ctx);
+
+ return warning("Node id missing at " + node.toString() + ". A blank node ID has been assigned", ctx);
+ } else if (!idNode.isTextual() || !PrimitiveValueValidator.validateIri(idNode)) {
+ return error("Embedded JSON object at " + node.asText() + " has no proper assigned id.", ctx);
+ } else if (assertion.getCredentialType() == Type.Assertion && !PrimitiveValueValidator.validateUrl(idNode)) {
+ if (!isUrn(idNode)) {
+ logger.info("ID format for " + idNode.toString() + " at " + assertion.getCredentialType() + " not in an expected HTTP or URN:UUID scheme");
+ }
+
+ // add a new node to the graph
+ JsonNode merged = createNewJson(ctx, node);
+ ctx.addGeneratedObject(new JsonLdGeneratedObject(JsonLDCompactionProve.getId(idNode.asText().strip()), merged.toString()));
+
+ // update existing node with new id
+ updateNode(validation, idNode, ctx);
+
+ } else {
+
+ // update existing node with new id
+ updateNode(validation, idNode, ctx);
+
+ // fetch node and add it to the graph
+ result = fetchNode(ctx, result, idNode);
+ }
+ }
+ }
+
+ List nodeList = JsonNodeUtil.asNodeList(node);
+ for (JsonNode childNode : nodeList) {
+ if (shouldFetch(childNode, validation)) {
+ // get node from context
+ result = fetchNode(ctx, result, childNode);
+ }
+ }
+
+ }
+ return success(ctx);
+ }
+
+ private ReportItems fetchNode(RunContext ctx, ReportItems result, JsonNode idNode)
+ throws URISyntaxException, Exception, JsonProcessingException, JsonMappingException {
+ System.out.println("fetchNode " + idNode.asText().strip());
+ UriResource uriResource = ((UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY)).of(idNode.asText().strip());
+ JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
+ if (resolved == null) {
+ System.out.println("parsing and loading " + idNode.asText().strip());
+ result = new ReportItems(List.of(result, new CredentialParseProbe().run(uriResource, ctx)));
+ if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
+ Assertion fetchedAssertion = (Assertion) ctx.getGeneratedObject(uriResource.getID());
+
+ // compact ld
+ result = new ReportItems(List.of(result, new JsonLDCompactionProve(fetchedAssertion.getCredentialType().getContextUris().get(0)).run(fetchedAssertion, ctx)));
+ if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
+ JsonLdGeneratedObject fetched = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(fetchedAssertion));
+ JsonNode fetchedNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER)).readTree(fetched.getJson());
+
+ // recursive call
+ result = new ReportItems(List.of(result, new GraphFetcherProbe(fetchedAssertion).run(fetchedNode, ctx)));
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Tells if we have to fetch the id. We have to fecth if:
+ * - the node is not a complex node
+ * - not (validation allow data-uri but the node is not of this type)
+ * - not (validation doesn't allow data-uri but the node is not an IRI)
+ * @param node
+ * @param validation
+ * @return
+ */
+ private boolean shouldFetch(JsonNode node, Validation validation) {
+ return !node.isObject() &&
+ (!validation.isAllowDataUri() || DATA_URI_OR_URL.getValidationFunction().apply(node)) &&
+ (validation.isAllowDataUri() || ValueType.IRI.getValidationFunction().apply(node));
+ }
+
+ private void updateNode(Validation validation, JsonNode idNode, RunContext ctx) throws IOException {
+ JsonLdGeneratedObject jsonLdGeneratedObject = ctx.getGeneratedObject(JsonLDCompactionProve.getId(assertion));
+ JsonNode merged = createNewJson(ctx, jsonLdGeneratedObject.getJson(), "{\"" + validation.getName() + "\": \"" + idNode.asText().strip() + "\"}");
+ jsonLdGeneratedObject.setJson(merged.toString());
+
+ }
+
+ private JsonNode createNewJson(RunContext ctx, JsonNode node) throws IOException {
+ return createNewJson(ctx, Resources.getResource("contexts/ob-v2p0.json"), node.toString());
+ }
+
+ private JsonNode createNewJson(RunContext ctx, String additional) throws IOException {
+ return createNewJson(ctx, Resources.getResource("contexts/ob-v2p0.json"), additional);
+ }
+
+ private JsonNode createNewJson(RunContext ctx, URL original, String additional) throws IOException {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode newNode = mapper.readTree(original);
+ ObjectReader readerForUpdating = mapper.readerForUpdating(newNode);
+ JsonNode merged = readerForUpdating.readValue(additional);
+ return merged;
+ }
+
+ private JsonNode createNewJson(RunContext ctx, String original, String updating) throws IOException {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode source = mapper.readTree(original);
+ ObjectReader readerForUpdating = mapper.readerForUpdating(source);
+ JsonNode merged = readerForUpdating.readValue(updating);
+ return merged;
+ }
+
+
+ private boolean isUrn(JsonNode idNode) {
+ final Pattern pattern = Pattern.compile(URN_REGEX, Pattern.CASE_INSENSITIVE);
+ final Matcher matcher = pattern.matcher(idNode.asText());
+ return matcher.matches();
+ }
+
+ public static final String ID = GraphFetcherProbe.class.getSimpleName();
+ public static final String URN_REGEX = "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'";
+ protected final static Logger logger = LogManager.getLogger(GraphFetcherProbe.class);
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java
new file mode 100644
index 0000000..1aeb5f5
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java
@@ -0,0 +1,69 @@
+package org.oneedtech.inspect.vc.jsonld.probe;
+
+import java.io.StringReader;
+
+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.report.ReportItems;
+import org.oneedtech.inspect.util.resource.Resource;
+import org.oneedtech.inspect.vc.Credential;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+
+import com.apicatalog.jsonld.JsonLd;
+import com.apicatalog.jsonld.JsonLdOptions;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.apicatalog.jsonld.loader.DocumentLoader;
+
+import jakarta.json.JsonObject;
+
+/**
+ * JSON-LD compaction probe for Open Badges 2.0
+ * Maps to "JSONLD_COMPACT_DATA" task in python implementation
+ * @author xaracil
+ */
+public class JsonLDCompactionProve extends Probe {
+ private final String context;
+
+ public JsonLDCompactionProve(String context) {
+ super(ID);
+ this.context = context;
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ try {
+ // compact JSON
+ JsonDocument jsonDocument = JsonDocument.of(new StringReader(crd.getJson().toString()));
+ JsonObject compactedObject = JsonLd.compact(jsonDocument, context)
+ .options(new JsonLdOptions((DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER)))
+ .get();
+
+ ctx.addGeneratedObject(new JsonLdGeneratedObject(getId(crd), compactedObject.toString()));
+
+ // Handle mismatch between URL node source and declared ID.
+ if (compactedObject.get("id") != null && crd.getResource().getID() != null
+ && !compactedObject.get("id").toString().equals(crd.getResource().getID())) {
+ // TODO: a new fetch of the JSON document at id is required
+ return warning("Node fetched from source " + crd.getResource().getID() + " declared its id as " + compactedObject.get("id").toString(), ctx);
+ }
+
+ return success(this, ctx);
+ } catch (Exception e) {
+ return fatal("Error while compacting JSON-LD: " + crd.getJson() + ". Caused by: " + e.getMessage(), ctx);
+ }
+ }
+
+ public static String getId(Credential crd) {
+ return getId(crd.getResource());
+ }
+
+ public static String getId(Resource resource) {
+ return getId(resource.getID());
+ }
+ public static String getId(String id) {
+ return "json-ld-compact:" + id;
+ }
+
+ public static final String ID = JsonLDCompactionProve.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDValidationProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDValidationProbe.java
new file mode 100644
index 0000000..9a2b1a4
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDValidationProbe.java
@@ -0,0 +1,33 @@
+package org.oneedtech.inspect.vc.jsonld.probe;
+
+import java.io.StringReader;
+
+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.jsonld.JsonLdGeneratedObject;
+
+import foundation.identity.jsonld.JsonLDObject;
+import foundation.identity.jsonld.validation.Validation;
+
+public class JsonLDValidationProbe extends Probe {
+ private final JsonLdGeneratedObject jsonLdObject;
+
+ public JsonLDValidationProbe(JsonLdGeneratedObject jsonLdObject) {
+ super();
+ this.jsonLdObject = jsonLdObject;
+ }
+
+ @Override
+ public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ JsonLDObject jsonLd = JsonLDObject.fromJson(new StringReader(jsonLdObject.getJson()));
+ try {
+ Validation.validate(jsonLd);
+ return success(this, ctx);
+ } catch (Exception e) {
+ return fatal("Error while validation JSON LD object: " + e.getMessage(), ctx);
+ }
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java
index 6552312..8c16ed3 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JsonParser.java
@@ -4,10 +4,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.vc.Credential;
-
import com.fasterxml.jackson.databind.JsonNode;
/**
@@ -22,11 +22,15 @@ public final class JsonParser extends PayloadParser {
}
@Override
- public Credential parse(Resource resource, RunContext ctx) throws Exception {
+ public 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);
+ JsonNode node = fromString(json, ctx);
+
+ return getBuilder(ctx)
+ .resource(resource)
+ .jsonData(node)
+ .build();
}
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java
index 3a946b1..1769d07 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/JwtParser.java
@@ -22,11 +22,15 @@ public final class JwtParser extends PayloadParser {
}
@Override
- public Credential parse(Resource resource, RunContext ctx) throws Exception {
+ 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);
+ JsonNode node = fromJwt(jwt, ctx);
+ return getBuilder(ctx)
+ .resource(resource)
+ .jsonData(node)
+ .jwt(jwt)
+ .build();
}
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java
index 41ccb2c..5ffd4c1 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PayloadParser.java
@@ -1,10 +1,13 @@
package org.oneedtech.inspect.vc.payload;
+import static com.apicatalog.jsonld.StringUtils.isBlank;
+
import java.util.Base64;
-import java.util.List;
import java.util.Base64.Decoder;
+import java.util.List;
import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.vc.Credential;
@@ -20,13 +23,18 @@ import com.google.common.base.Splitter;
public abstract class PayloadParser {
public abstract boolean supports(ResourceType type);
-
+
public abstract Credential parse(Resource source, RunContext ctx) throws Exception;
+ @SuppressWarnings("rawtypes")
+ public static Credential.Builder getBuilder(RunContext context) {
+ return ((Credential.Builder) context.get(RunContext.Key.GENERATED_OBJECT_BUILDER));
+ }
+
protected static JsonNode fromString(String json, RunContext context) throws Exception {
return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json);
}
-
+
/**
* Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof
* @return The decoded JSON String
@@ -34,19 +42,23 @@ public abstract class PayloadParser {
public static JsonNode fromJwt(String jwt, RunContext context) throws Exception {
List parts = Splitter.on('.').splitToList(jwt);
if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
-
+
final Decoder decoder = Base64.getUrlDecoder();
/*
* For this step we are only deserializing the stored badge out of the payload.
- * The entire jwt is stored separately for signature verification later.
+ * The entire jwt is stored separately for signature verification later.
*/
String jwtPayload = new String(decoder.decode(parts.get(1)));
- //Deserialize and fetch the 'vc' node from the object
+ //Deserialize and fetch the credential node from the object
JsonNode outerPayload = fromString(jwtPayload, context);
- JsonNode vcNode = outerPayload.get("vc");
+ String nodeName = (String) context.get(Key.JWT_CREDENTIAL_NODE_NAME);
+ if (isBlank(nodeName)) {
+ return outerPayload;
+ }
+ JsonNode vcNode = outerPayload.get(nodeName);
return vcNode;
}
-
+
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java
index f521e47..492bab2 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/PngParser.java
@@ -30,11 +30,12 @@ public final class PngParser extends PayloadParser {
@Override
public Credential parse(Resource resource, RunContext ctx) throws Exception {
-
+
checkTrue(resource.getType() == ResourceType.PNG);
-
+
try(InputStream is = resource.asByteSource().openStream()) {
-
+ final Keys credentialKey = (Keys) ctx.get(RunContext.Key.PNG_CREDENTIAL_KEY);
+
ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next();
imageReader.setInput(ImageIO.createImageInputStream(is), true);
IIOMetadata metadata = imageReader.getImageMetadata(0);
@@ -43,20 +44,20 @@ public final class PngParser extends PayloadParser {
String jwtString = null;
String formatSearch = null;
JsonNode vcNode = null;
-
+
String[] names = metadata.getMetadataFormatNames();
int length = names.length;
for (int i = 0; i < length; i++) {
//Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0")
- formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]));
- if(formatSearch != null) {
- vcString = formatSearch;
+ formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]), credentialKey);
+ if(formatSearch != null) {
+ vcString = formatSearch;
break;
}
}
- if(vcString == null) {
- throw new IllegalArgumentException("No credential inside PNG");
+ if(vcString == null) {
+ throw new IllegalArgumentException("No credential inside PNG");
}
vcString = vcString.trim();
@@ -68,29 +69,33 @@ public final class PngParser extends PayloadParser {
else {
vcNode = fromString(vcString, ctx);
}
-
- return new Credential(resource, vcNode, jwtString);
+
+ return getBuilder(ctx)
+ .resource(resource)
+ .jsonData(vcNode)
+ .jwt(jwtString)
+ .build();
}
}
-
- private String getOpenBadgeCredentialNodeText(Node node){
+
+ private String getOpenBadgeCredentialNodeText(Node node, Keys credentialKey){
NamedNodeMap attributes = node.getAttributes();
//If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one.
Node keyword = attributes.getNamedItem("keyword");
- if(keyword != null && keyword.getNodeValue().equals("openbadgecredential")){
+ if(keyword != null && keyword.getNodeValue().equals(credentialKey.getNodeName())){
Node textAttribute = attributes.getNamedItem("text");
- if(textAttribute != null) {
- return textAttribute.getNodeValue();
+ if(textAttribute != null) {
+ return textAttribute.getNodeValue();
}
}
//iterate over all children depth first and search for the credential node.
Node child = node.getFirstChild();
while (child != null) {
- String nodeValue = getOpenBadgeCredentialNodeText(child);
- if(nodeValue != null) {
- return nodeValue;
+ String nodeValue = getOpenBadgeCredentialNodeText(child, credentialKey);
+ if(nodeValue != null) {
+ return nodeValue;
}
child = child.getNextSibling();
}
@@ -99,4 +104,19 @@ public final class PngParser extends PayloadParser {
return null;
}
+ public enum Keys {
+ OB20("openbadges"),
+ OB30("openbadgecredential"),
+ CLR20("clrcredential");
+
+ private String nodeName;
+
+ private Keys(String nodeName) {
+ this.nodeName = nodeName;
+ }
+
+ public String getNodeName() {
+ return nodeName;
+ }
+ }
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java
index f600bc9..8d9a659 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/payload/SvgParser.java
@@ -11,7 +11,6 @@ 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;
@@ -32,48 +31,74 @@ public final class SvgParser extends PayloadParser {
@Override
public Credential parse(Resource resource, RunContext ctx) throws Exception {
-
+ final QNames qNames = (QNames) ctx.get(RunContext.Key.SVG_CREDENTIAL_QNAME);
+
checkTrue(resource.getType() == ResourceType.SVG);
-
+
try(InputStream is = resource.asByteSource().openStream()) {
XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is);
while(reader.hasNext()) {
XMLEvent ev = reader.nextEvent();
- if(isEndElem(ev, OB_CRED_ELEM)) break;
- if(isStartElem(ev, OB_CRED_ELEM)) {
- Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR);
+ if(isEndElem(ev, qNames.getCredentialElem())) break;
+ if(isStartElem(ev, qNames.getCredentialElem())) {
+ Attribute verifyAttr = ev.asStartElement().getAttributeByName(qNames.getVerifyElem());
if(verifyAttr != null) {
String jwt = verifyAttr.getValue();
JsonNode node = fromJwt(jwt, ctx);
- return new Credential(resource, node, jwt);
+ return getBuilder(ctx).resource(resource).jsonData(node).jwt(jwt).build();
} else {
while(reader.hasNext()) {
ev = reader.nextEvent();
- if(isEndElem(ev, OB_CRED_ELEM)) break;
+ if(isEndElem(ev, qNames.getCredentialElem())) break;
if(ev.getEventType() == XMLEvent.CHARACTERS) {
Characters chars = ev.asCharacters();
- if(!chars.isWhiteSpace()) {
+ if(!chars.isWhiteSpace()) {
JsonNode node = fromString(chars.getData(), ctx);
- return new Credential(resource, node);
+ return getBuilder(ctx).resource(resource).jsonData(node).build();
}
}
- }
+ }
}
}
} //while(reader.hasNext()) {
}
- throw new IllegalArgumentException("No credential inside SVG");
-
+ throw new IllegalArgumentException("No credential inside SVG");
+
}
private boolean isEndElem(XMLEvent ev, QName name) {
return ev.isEndElement() && ev.asEndElement().getName().equals(name);
}
-
+
private boolean isStartElem(XMLEvent ev, QName name) {
return ev.isStartElement() && ev.asStartElement().getName().equals(name);
}
-
- private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential");
+
private static final QName OB_CRED_VERIFY_ATTR = new QName("verify");
+
+ /*
+ * Know QNames whre the credential is baked into the SVG
+ */
+ public enum QNames {
+ OB20(new QName("http://openbadges.org", "assertion"), OB_CRED_VERIFY_ATTR),
+ OB30(new QName("https://purl.imsglobal.org/ob/v3p0", "credential"), OB_CRED_VERIFY_ATTR),
+ CLR20(new QName("https://purl.imsglobal.org/clr/v2p0", "credential"), OB_CRED_VERIFY_ATTR);
+
+ private final QName credentialElem;
+ private final QName verifyElem;
+
+ private QNames(QName credentialElem, QName verifyElem) {
+ this.credentialElem = credentialElem;
+ this.verifyElem = verifyElem;
+ }
+
+ public QName getCredentialElem() {
+ return credentialElem;
+ }
+
+ public QName getVerifyElem() {
+ return verifyElem;
+ }
+
+ }
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/AssertionRevocationListProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/AssertionRevocationListProbe.java
new file mode 100644
index 0000000..d91abe3
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/AssertionRevocationListProbe.java
@@ -0,0 +1,107 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+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.report.ReportItems;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+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;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+public class AssertionRevocationListProbe extends Probe {
+ private final String assertionId;
+ private final String propertyName;
+
+ public AssertionRevocationListProbe(String assertionId) {
+ this(assertionId, "badge");
+ }
+
+ public AssertionRevocationListProbe(String assertionId, String propertyName) {
+ super(ID);
+ this.assertionId = assertionId;
+ this.propertyName = propertyName;
+ }
+
+ @Override
+ public ReportItems run(JsonLdGeneratedObject jsonLdGeneratedObject, RunContext ctx) throws Exception {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode jsonNode = (mapper).readTree(jsonLdGeneratedObject.getJson());
+ UriResourceFactory uriResourceFactory = (UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY);
+
+ // get badge
+ UriResource badgeUriResource = uriResourceFactory.of(getBadgeClaimId(jsonNode));
+ JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(badgeUriResource));
+
+ // get issuer from badge
+ JsonNode badgeNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(badgeObject.getJson());
+
+ UriResource issuerUriResource = uriResourceFactory.of(badgeNode.get("issuer").asText().strip());
+ JsonLdGeneratedObject issuerObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(issuerUriResource));
+ JsonNode issuerNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(issuerObject.getJson());
+
+ JsonNode revocationListIdNode = issuerNode.get("revocationList");
+ if (revocationListIdNode == null) {
+ // "Assertion is not revoked. Issuer has no revocation list"
+ return success(ctx);
+ }
+
+ UriResource revocationListUriResource = uriResourceFactory.of(revocationListIdNode.asText().strip());
+ JsonLdGeneratedObject revocationListObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(revocationListUriResource));
+ JsonNode revocationListNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(revocationListObject.getJson());
+
+ List revocationList = JsonNodeUtil.asNodeList(revocationListNode.get("revokedAssertions"));
+ List revokedMatches = revocationList.stream().filter(revocation -> {
+ if (revocation.isTextual()) {
+ return assertionId.equals(revocation.asText().strip());
+ }
+ return revocation.get("id") != null && assertionId.equals(revocation.get("id").asText().strip());
+ }).collect(Collectors.toList());
+
+ if (revokedMatches.size() > 0) {
+ Optional reasonNode = revokedMatches.stream()
+ .map(node -> node.get("revocationReason"))
+ .filter(Objects::nonNull)
+ .findFirst();
+ String reason = reasonNode.isPresent() ? " with reason " + reasonNode.get().asText().strip() : "";
+ return error("Assertion " + assertionId + " has been revoked in RevocationList " + revocationListIdNode.asText().strip() + reason, ctx);
+ }
+ return success(ctx);
+ }
+
+ /**
+ * Return the ID of the node with name propertyName
+ * @param jsonNode node
+ * @return ID of the node. If node is textual, the text is returned. If node is an object, its "ID" attribute is returned
+ */
+ protected String getBadgeClaimId(JsonNode jsonNode) {
+ JsonNode propertyNode = jsonNode.get(propertyName);
+ if (propertyNode.isTextual()) {
+ return propertyNode.asText().strip();
+ }
+ return propertyNode.get("id").asText().strip();
+ }
+
+ public static final String ID = AssertionRevocationListProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java
index 87a1999..f0ec2ab 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ContextPropertyProbe.java
@@ -1,76 +1,50 @@
package org.oneedtech.inspect.vc.probe;
-import static org.oneedtech.inspect.vc.Credential.Type.*;
-
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
-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 org.oneedtech.inspect.vc.Credential.CredentialEnum;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.google.common.collect.ImmutableMap;
/**
* A Probe that verifies a credential's context property.
- *
+ *
* @author mgylling
*/
-public class ContextPropertyProbe extends Probe {
- private final Credential.Type type;
+public class ContextPropertyProbe extends StringValuePropertyProbe {
+ private final CredentialEnum type;
- public ContextPropertyProbe(Credential.Type type) {
- super(ID);
+ public ContextPropertyProbe(CredentialEnum type) {
+ super(ID, type.toString(), "@context");
this.type = checkNotNull(type);
+ setValueValidations(this::validate);
}
@Override
- public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+ protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
+ return notRun("No @context property", ctx);
+ }
- ArrayNode contextNode = (ArrayNode) root.get("@context");
- if (contextNode == null) {
- return notRun("No @context property", ctx);
- }
+ public ReportItems validate(List nodeValues, RunContext ctx) {
+ if (!nodeValues.isEmpty()) { // empty context uri node: inline context
+ List contextUris = type.getContextUris();
+ checkNotNull(contextUris);
- List expected = values.get(values.keySet()
- .stream()
- .filter(s->s.contains(type))
- .findFirst()
- .orElseThrow(()-> new IllegalArgumentException(type.name() + " not recognized")));
-
- List given = JsonNodeUtil.asStringList(contextNode);
- int pos = 0;
- for (String uri : expected) {
- if ((given.size() < pos + 1) || !given.get(pos).equals(uri)) {
- return error("missing required @context uri " + uri + " at position " + (pos + 1), ctx);
+ int pos = 0;
+ for (String uri : contextUris) {
+ if ((nodeValues.size() < pos + 1) || !nodeValues.get(pos).equals(uri)) {
+ return error("missing required @context uri " + uri + " at position " + (pos + 1), ctx);
+ }
+ pos++;
}
- pos++;
}
return success(ctx);
}
- private final static Map, List> values = new ImmutableMap.Builder, List>()
- .put(Set.of(OpenBadgeCredential, AchievementCredential, EndorsementCredential),
- List.of("https://www.w3.org/2018/credentials/v1",
- //"https://purl.imsglobal.org/spec/ob/v3p0/context.json")) //dev legacy
- "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
- .put(Set.of(ClrCredential),
- List.of("https://www.w3.org/2018/credentials/v1",
-// "https://dc.imsglobal.org/draft/clr/v2p0/context", //dev legacy
-// "https://purl.imsglobal.org/spec/ob/v3p0/context.json")) //dev legacy
- "https://purl.imsglobal.org/spec/clr/v2p0/context.json",
- "https://purl.imsglobal.org/spec/ob/v3p0/context.json"))
-
- .build();
-
public static final String ID = ContextPropertyProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java
index 02a549b..1444c50 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/CredentialParseProbe.java
@@ -9,25 +9,26 @@ 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.VerifiableCredential;
import org.oneedtech.inspect.vc.payload.PayloadParserFactory;
/**
* A probe that verifies that the incoming credential resource is of a recognized payload type
- * and if so extracts and stores the VC json data (a 'Credential' instance)
- * in the RunContext.
+ * and if so extracts and stores the VC json data (a 'Credential' instance)
+ * in the RunContext.
* @author mgylling
*/
public class CredentialParseProbe extends Probe {
-
+
@Override
public ReportItems run(Resource resource, RunContext context) throws Exception {
-
+
try {
-
+
//TODO if .detect reads from a URIResource twice. Cache the resource on first call.
-
+
Optional type = Optional.ofNullable(resource.getType());
- if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) {
+ if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) {
type = TypeDetector.detect(resource, true);
if(type.isEmpty()) {
//TODO if URI fetch, TypeDetector likely to fail
@@ -37,18 +38,18 @@ public class CredentialParseProbe extends Probe {
resource.setType(type.get());
}
}
-
- if(!Credential.RECOGNIZED_PAYLOAD_TYPES.contains(type.get())) {
+
+ if(!VerifiableCredential.RECOGNIZED_PAYLOAD_TYPES.contains(type.get())) {
return fatal("Payload type not supported: " + type.get().getName(), context);
}
-
- Credential crd = PayloadParserFactory.of(resource).parse(resource, context);
+
+ Credential crd = PayloadParserFactory.of(resource).parse(resource, context);
context.addGeneratedObject(crd);
return success(this, context);
-
+
} catch (Exception e) {
return fatal("Error while parsing credential: " + e.getMessage(), context);
- }
+ }
}
-
+
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
index b3fe3b4..b70d1c3 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/EmbeddedProofProbe.java
@@ -7,7 +7,8 @@ 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.vc.Credential;
+import org.oneedtech.inspect.vc.VerifiableCredential;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import com.apicatalog.jsonld.StringUtils;
import com.apicatalog.jsonld.document.Document;
@@ -15,9 +16,7 @@ import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.apicatalog.multibase.Multibase;
import com.apicatalog.multicodec.Multicodec;
import com.apicatalog.multicodec.Multicodec.Codec;
-import com.danubetech.verifiablecredentials.VerifiableCredential;
-import foundation.identity.jsonld.ConfigurableDocumentLoader;
import info.weboftrust.ldsignatures.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
import jakarta.json.JsonObject;
@@ -28,7 +27,7 @@ import jakarta.json.JsonStructure;
*
* @author mgylling
*/
-public class EmbeddedProofProbe extends Probe {
+public class EmbeddedProofProbe extends Probe {
public EmbeddedProofProbe() {
super(ID);
@@ -39,15 +38,12 @@ public class EmbeddedProofProbe extends Probe {
* (https://github.com/danubetech/verifiable-credentials-java)
*/
@Override
- public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
// TODO: What there are multiple proofs?
- VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString()));
- ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader();
- documentLoader.setEnableHttp(true);
- documentLoader.setEnableHttps(true);
- vc.setDocumentLoader(documentLoader);
+ com.danubetech.verifiablecredentials.VerifiableCredential vc = com.danubetech.verifiablecredentials.VerifiableCredential.fromJson(new StringReader(crd.getJson().toString()));
+ vc.setDocumentLoader(new CachingDocumentLoader());
LdProof proof = vc.getLdProof();
if (proof == null) {
@@ -97,7 +93,7 @@ public class EmbeddedProofProbe extends Probe {
if (keyStructure.isEmpty()) {
return error("Key document not found at " + method, ctx);
}
-
+
// First look for a Ed25519VerificationKey2020 document
controller = keyStructure.get().asJsonObject().getString("controller");
if (StringUtils.isBlank(controller)) {
@@ -113,7 +109,7 @@ public class EmbeddedProofProbe extends Probe {
} else {
publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase");
}
-
+
} catch (Exception e) {
return error("Invalid verification key URL: " + e.getMessage(), ctx);
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java
index fcebd0c..01e6fb0 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExpirationProbe.java
@@ -10,34 +10,34 @@ import org.oneedtech.inspect.vc.Credential;
import com.fasterxml.jackson.databind.JsonNode;
/**
- * A Probe that verifies a credential's expiration status
+ * A Probe that verifies a credential's expiration status
* @author mgylling
*/
public class ExpirationProbe extends Probe {
-
+
public ExpirationProbe() {
super(ID);
}
-
+
@Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception {
- /*
- * If the AchievementCredential or EndorsementCredential has an “expirationDate” property
- * and the expiration date is prior to the current date, the credential has expired.
- */
- JsonNode node = crd.getJson().get("expirationDate");
+ /*
+ * If the AchievementCredential or EndorsementCredential has an “expirationDate” property
+ * and the expiration date is prior to the current date, the credential has expired.
+ */
+ JsonNode node = crd.getJson().get(crd.getExpiresAtPropertyName());
if(node != null) {
try {
ZonedDateTime expirationDate = ZonedDateTime.parse(node.textValue());
if (ZonedDateTime.now().isAfter(expirationDate)) {
return fatal("The credential has expired (expiration date was " + node.asText() + ").", ctx);
- }
+ }
} catch (Exception e) {
- return exception("Error while checking expirationDate: " + e.getMessage(), ctx.getResource());
+ return exception("Error while checking expirationDate: " + e.getMessage(), ctx.getResource());
}
}
return success(ctx);
}
-
- public static final String ID = ExpirationProbe.class.getSimpleName();
+
+ public static final String ID = ExpirationProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java
index b4bccb0..b14ce3c 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ExternalProofProbe.java
@@ -21,7 +21,7 @@ import org.apache.http.util.EntityUtils;
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.VerifiableCredential;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
@@ -36,32 +36,32 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Splitter;
/**
- * A Probe that verifies credential external proof (jwt)
+ * A Probe that verifies credential external proof (jwt)
* @author mlyon
*/
-public class ExternalProofProbe extends Probe {
-
+public class ExternalProofProbe extends Probe {
+
public ExternalProofProbe() {
super(ID);
}
-
+
@Override
- public ReportItems run(Credential crd, RunContext ctx) throws Exception {
+ public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
try {
verifySignature(crd, ctx);
} catch (Exception e) {
return fatal("Error verifying jwt signature: " + e.getMessage(), ctx);
- }
+ }
return success(ctx);
}
- private void verifySignature(Credential crd, RunContext ctx) throws Exception {
+ private void verifySignature(VerifiableCredential crd, RunContext ctx) throws Exception {
checkTrue(crd.getJwt().isPresent(), "no jwt supplied");
checkTrue(crd.getJwt().get().length() > 0, "no jwt supplied");
-
+
DecodedJWT decodedJwt = null;
String jwt = crd.getJwt().get();
-
+
List parts = Splitter.on('.').splitToList(jwt);
if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
@@ -85,7 +85,7 @@ public class ExternalProofProbe extends Probe {
if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); }
if(kid != null){
- //Load jwk JsonNode from url and do the rest the same below.
+ //Load jwk JsonNode from url and do the rest the same below.
//TODO Consider additional testing.
String kidUrl = kid.textValue();
String jwkResponse = fetchJwk(kidUrl);
@@ -145,7 +145,7 @@ public class ExternalProofProbe extends Probe {
return responseString;
}
-
+
public static final String ID = ExternalProofProbe.class.getSimpleName();
}
\ No newline at end of file
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
index 7f3f86e..30c4324 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/InlineJsonSchemaProbe.java
@@ -11,64 +11,64 @@ import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe;
import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.schema.SchemaKey;
-import org.oneedtech.inspect.vc.Credential;
+import org.oneedtech.inspect.vc.VerifiableCredential;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
/**
- * Detect inline schemas in a credential and run them.
+ * Detect inline schemas in a credential and run them.
* @author mgylling
*/
public class InlineJsonSchemaProbe extends Probe {
private static final Set types = Set.of("1EdTechJsonSchemaValidator2019");
private SchemaKey skip;
-
+
public InlineJsonSchemaProbe() {
super(ID);
}
-
+
public InlineJsonSchemaProbe(SchemaKey skip) {
super(ID);
this.skip = skip;
}
-
+
@Override
public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
List accumulator = new ArrayList<>();
- Set ioErrors = new HashSet<>();
+ Set ioErrors = new HashSet<>();
- //note - we don't get deep nested ones in e.g. EndorsementCredential
+ //note - we don't get deep nested ones in e.g. EndorsementCredential
JsonNode credentialSchemaNode = root.get("credentialSchema");
if(credentialSchemaNode == null) return success(ctx);
-
+
ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast
-
+
for(JsonNode schemaNode : schemas) {
JsonNode typeNode = schemaNode.get("type");
- if(typeNode == null || !types.contains(typeNode.asText())) continue;
- JsonNode idNode = schemaNode.get("id");
+ if(typeNode == null || !types.contains(typeNode.asText())) continue;
+ JsonNode idNode = schemaNode.get("id");
if(idNode == null) continue;
- String id = idNode.asText().strip();
- if(ioErrors.contains(id)) continue;
- if(equals(skip, id)) continue;
- try {
+ String id = idNode.asText().strip();
+ if(ioErrors.contains(id)) continue;
+ if(equals(skip, id)) continue;
+ try {
accumulator.add(new JsonSchemaProbe(id).run(root, ctx));
- } catch (Exception e) {
+ } catch (Exception e) {
if(!ioErrors.contains(id)) {
ioErrors.add(id);
accumulator.add(error("Could not read schema resource " + id, ctx));
- }
- }
+ }
+ }
}
-
+
return new ReportItems(accumulator);
}
-
+
private boolean equals(SchemaKey key, String id) {
if(key == null) return false;
- return key.getCanonicalURI().equals(id);
+ return key.getCanonicalURI().equals(id);
}
-
+
public static final String ID = InlineJsonSchemaProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java
index 04c5d6b..2328199 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/IssuanceProbe.java
@@ -10,34 +10,34 @@ import org.oneedtech.inspect.vc.Credential;
import com.fasterxml.jackson.databind.JsonNode;
/**
- * A Probe that verifies a credential's issuance status
+ * A Probe that verifies a credential's issuance status
* @author mgylling
*/
public class IssuanceProbe extends Probe {
-
+
public IssuanceProbe() {
super(ID);
}
-
+
@Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception {
- /*
- * If the AchievementCredential or EndorsementCredential “issuanceDate”
+ /*
+ * If the AchievementCredential or EndorsementCredential “issuanceDate”
* property after the current date, the credential is not yet valid.
*/
- JsonNode node = crd.getJson().get("issuanceDate");
+ JsonNode node = crd.getJson().get(crd.getIssuedOnPropertyName());
if(node != null) {
try {
- ZonedDateTime issuanceDate = ZonedDateTime.parse(node.textValue());
+ ZonedDateTime issuanceDate = ZonedDateTime.parse(node.textValue());
if (issuanceDate.isAfter(ZonedDateTime.now())) {
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());
}
}
return success(ctx);
- }
-
- public static final String ID = IssuanceProbe.class.getSimpleName();
+ }
+
+ public static final String ID = IssuanceProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/PropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/PropertyProbe.java
new file mode 100644
index 0000000..cde8cb5
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/PropertyProbe.java
@@ -0,0 +1,42 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.util.function.BiFunction;
+
+import org.oneedtech.inspect.core.probe.Probe;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.ReportItems;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class PropertyProbe extends Probe {
+ private final String propertyName;
+ private BiFunction validations;
+
+ public PropertyProbe(String id, String typeName, String propertyName) {
+ super(id, typeName, propertyName);
+ this.propertyName = propertyName;
+ this.validations = this::defaultValidation;
+ }
+
+ public void setValidations(BiFunction validations) {
+ this.validations = validations;
+ }
+
+ @Override
+ public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
+ JsonNode propertyNode = root.get(propertyName);
+ if (propertyNode == null) {
+ return reportForNonExistentProperty(root, ctx);
+ }
+
+ return validations.apply(propertyNode, ctx);
+ }
+
+ protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
+ return fatal("No " + propertyName + " property", ctx);
+ }
+
+ private ReportItems defaultValidation(JsonNode node, RunContext ctx) {
+ return success(ctx);
+ }
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java
index ca11b0f..e083c42 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/RevocationListProbe.java
@@ -11,6 +11,7 @@ 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.VerifiableCredential;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.fasterxml.jackson.databind.JsonNode;
@@ -21,23 +22,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author mgylling
*/
public class RevocationListProbe extends Probe {
-
+
public RevocationListProbe() {
super(ID);
}
-
+
@Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception {
/*
- * If the AchievementCredential or EndorsementCredential has a “credentialStatus” property
- * and the type of the CredentialStatus object is “1EdTechRevocationList”, fetch the
- * credential status from the URL provided. If the request is unsuccessful,
- * report a warning, not an error.
+ * If the AchievementCredential or EndorsementCredential has a “credentialStatus” property
+ * and the type of the CredentialStatus object is “1EdTechRevocationList”, fetch the
+ * credential status from the URL provided. If the request is unsuccessful,
+ * report a warning, not an error.
*/
-
+
JsonNode credentialStatus = crd.getJson().get("credentialStatus");
- if(credentialStatus != null) {
+ if(credentialStatus != null) {
JsonNode type = credentialStatus.get("type");
if(type != null && type.asText().strip().equals("1EdTechRevocationList")) {
JsonNode listID = credentialStatus.get("id");
@@ -46,34 +47,34 @@ public class RevocationListProbe extends Probe {
URL url = new URI(listID.asText().strip()).toURL();
try (InputStream is = url.openStream()) {
JsonNode revocList = ((ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER)).readTree(is.readAllBytes());
-
- /* To check if a credential has been revoked, the verifier issues a GET request
- * to the URL of the issuer's 1EdTech Revocation List Status Method. If the
- * credential's id is in the list of revokedCredentials and the value of
+
+ /* To check if a credential has been revoked, the verifier issues a GET request
+ * to the URL of the issuer's 1EdTech Revocation List Status Method. If the
+ * credential's id is in the list of revokedCredentials and the value of
* revoked is true or ommitted, the issuer has revoked the credential. */
-
+
JsonNode crdID = crd.getJson().get("id"); //TODO these != checks sb removed (trigger warning)
if(crdID != null) {
List list = JsonNodeUtil.asNodeList(revocList.get("revokedCredentials"));
if(list != null) {
for(JsonNode item : list) {
JsonNode revID = item.get("id");
- JsonNode revoked = item.get("revoked");
+ JsonNode revoked = item.get("revoked");
if(revID != null && revID.equals(crdID) && (revoked == null || revoked.asBoolean())) {
- return fatal("Credential has been revoked", ctx);
+ return fatal("Credential has been revoked", ctx);
}
- }
- }
+ }
+ }
}
- }
+ }
} catch (Exception e) {
return warning("Error when fetching credentialStatus resource " + e.getMessage(), ctx);
- }
- }
- }
+ }
+ }
+ }
}
return success(ctx);
- }
-
- public static final String ID = RevocationListProbe.class.getSimpleName();
+ }
+
+ public static final String ID = RevocationListProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/StringValuePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/StringValuePropertyProbe.java
new file mode 100644
index 0000000..de249c9
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/StringValuePropertyProbe.java
@@ -0,0 +1,35 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class StringValuePropertyProbe extends PropertyProbe {
+ private BiFunction, RunContext, ReportItems> valueValidations;
+
+ public StringValuePropertyProbe(String id, String credentialType, String propertyName) {
+ super(id, credentialType, propertyName);
+ this.valueValidations = this::defaultValidation;
+ super.setValidations(this::nodeValidation);
+ }
+
+ public void setValueValidations(BiFunction, RunContext, ReportItems> validations) {
+ this.valueValidations = validations;
+ }
+
+ private ReportItems nodeValidation(JsonNode node, RunContext ctx) {
+ List values = JsonNodeUtil.asStringList(node);
+ return valueValidations.apply(values, ctx);
+}
+
+ private ReportItems defaultValidation(List nodeValues, RunContext ctx) {
+ return success(ctx);
+ }
+
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java
index 804380e..7814f46 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/TypePropertyProbe.java
@@ -3,62 +3,61 @@ package org.oneedtech.inspect.vc.probe;
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import java.util.List;
+import java.util.stream.Collectors;
-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.Credential.Type;
-import org.oneedtech.inspect.vc.util.JsonNodeUtil;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
+import org.oneedtech.inspect.vc.Credential.CredentialEnum;
/**
* A Probe that verifies a credential's type property.
- *
+ *
* @author mgylling
*/
-public class TypePropertyProbe extends Probe {
- private final Credential.Type expected;
+public class TypePropertyProbe extends StringValuePropertyProbe {
+ private final CredentialEnum expected;
- public TypePropertyProbe(Credential.Type expected) {
- super(ID);
+ public TypePropertyProbe(CredentialEnum expected) {
+ super(ID, expected.toString(), "type");
this.expected = checkNotNull(expected);
+ this.setValueValidations(this::validate);
}
- @Override
- public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
-
- ArrayNode typeNode = (ArrayNode) root.get("type");
- if (typeNode == null)
- return fatal("No type property", ctx);
-
- List values = JsonNodeUtil.asStringList(typeNode);
-
- if (!values.contains("VerifiableCredential")) {
- return fatal("The type property does not contain the entry 'VerifiableCredential'", ctx);
+ public ReportItems validate(List values, RunContext ctx) {
+ List requiredTypeValues = expected.getRequiredTypeValues();
+ if (!requiredTypeValues.isEmpty()) {
+ if (!requiredTypeValues.stream().allMatch(requiredValue -> values.contains(requiredValue))) {
+ return fatal(formatMessage(requiredTypeValues), 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);
+ if (expected.isAllowedTypeValuesRequired()) {
+ List allowedValues = expected.getAllowedTypeValues();
+ if (allowedValues.isEmpty()) {
+ return fatal("The type property is invalid", ctx);
}
- } else if (expected == Credential.Type.ClrCredential) {
- if (!values.contains("ClrCredential")) {
- return fatal("The type property does not contain the entry 'ClrCredential'", ctx);
+ if (!values.stream().anyMatch(v -> allowedValues.contains(v))) {
+ return fatal(formatMessage(values), ctx);
}
- } else if (expected == Credential.Type.EndorsementCredential) {
- if (!values.contains("EndorsementCredential")) {
- return fatal("The type property does not contain the entry 'EndorsementCredential'", ctx);
- }
- } else {
- // TODO implement
- throw new IllegalStateException();
}
return success(ctx);
}
+ private String formatMessage(List values) {
+ StringBuffer buffer = new StringBuffer("The type property does not contain ");
+ if (values.size() > 1) {
+ buffer.append("one of");
+ buffer.append(values.stream()
+ .map(value -> "'" + value + "'")
+ .collect(Collectors.joining(" or "))
+ );
+
+ } else {
+ buffer.append("the entry '" + values.get(0) + "'");
+ }
+ return buffer.toString();
+ }
+
public static final String ID = TypePropertyProbe.class.getSimpleName();
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java
new file mode 100644
index 0000000..8011742
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java
@@ -0,0 +1,157 @@
+package org.oneedtech.inspect.vc.probe;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+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.report.ReportItems;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Verification probe for Open Badges 2.0
+ * Maps to "ASSERTION_VERIFICATION_DEPENDENCIES" task in python implementation
+ * @author xaracil
+ */
+public class VerificationDependenciesProbe extends Probe {
+ private final String assertionId;
+ private final String propertyName;
+
+ public VerificationDependenciesProbe(String assertionId) {
+ this(assertionId, "badge");
+ }
+
+ public VerificationDependenciesProbe(String assertionId, String propertyName) {
+ super(ID);
+ this.assertionId = assertionId;
+ this.propertyName = propertyName;
+ }
+
+
+ @Override
+ public ReportItems run(JsonLdGeneratedObject jsonLdGeneratedObject, RunContext ctx) throws Exception {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode jsonNode = (mapper).readTree(jsonLdGeneratedObject.getJson());
+ UriResourceFactory uriResourceFactory = (UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY);
+
+ JsonNode verificationNode = jsonNode.get("verification");
+ checkNotNull(verificationNode);
+ String type = null;
+ if (verificationNode.isTextual()) {
+ // get verification from graph
+ UriResource verificationUriResource = uriResourceFactory.of(verificationNode.asText().strip());
+ JsonLdGeneratedObject verificationObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(verificationUriResource));
+ JsonNode verificationRootNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(verificationObject.getJson());
+ type = verificationRootNode.get("type").asText().strip();
+ } else {
+ type = verificationNode.get("type").asText().strip();
+ }
+
+ if ("HostedBadge".equals(type)) {
+ // get badge
+ UriResource badgeUriResource = uriResourceFactory.of(getBadgeClaimId(jsonNode));
+ JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(badgeUriResource));
+ JsonNode badgeNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(badgeObject.getJson());
+
+ // get issuer from badge
+ UriResource issuerUriResource = uriResourceFactory.of(badgeNode.get("issuer").asText().strip());
+
+ JsonLdGeneratedObject issuerObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(issuerUriResource));
+ JsonNode issuerNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(issuerObject.getJson());
+
+ // verify issuer
+ JsonNode verificationPolicy = issuerNode.get("verification");
+ try {
+ checkNotNull(verificationPolicy);
+ if (verificationPolicy.isTextual()) {
+ // get verification node
+ JsonLdGeneratedObject verificationPolicyObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(verificationPolicy.asText().strip()));
+ verificationPolicy = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(verificationPolicyObject.getJson());
+ }
+ } catch (Throwable t) {
+ verificationPolicy = getDefaultVerificationPolicy(issuerNode, mapper);
+ }
+
+ // starts with check
+ if (verificationPolicy.has("startsWith")) {
+ List startsWith = JsonNodeUtil.asStringList(verificationPolicy.get("startsWith"));
+ if (!startsWith.stream().anyMatch(assertionId::startsWith)) {
+ return error("Assertion id " + assertionId
+ + "does not start with any permitted values in its issuer's verification policy.", ctx);
+ }
+ }
+
+ // allowed origins
+ JsonNode allowedOriginsNode = verificationPolicy.get("allowedOrigins");
+ List allowedOrigins = null;
+ String issuerId = issuerNode.get("id").asText().strip();
+ if (allowedOriginsNode == null || allowedOriginsNode.isNull()) {
+ String defaultAllowedOrigins = getDefaultAllowedOrigins(issuerId);
+ if (defaultAllowedOrigins != null) {
+ allowedOrigins = List.of(defaultAllowedOrigins);
+ }
+ } else {
+ allowedOrigins = JsonNodeUtil.asStringList(allowedOriginsNode);
+ }
+
+ if (allowedOrigins == null || allowedOrigins.isEmpty() || !issuerId.startsWith("http")) {
+ return warning("Issuer " + issuerId + " has no HTTP domain to enforce hosted verification policy against.", ctx);
+ }
+
+ if (!allowedOrigins.contains(new URI(assertionId).getAuthority())) {
+ return error("Assertion " + assertionId + " not hosted in allowed origins " + allowedOrigins, ctx);
+ }
+ }
+ return success(ctx);
+ }
+
+ private JsonNode getDefaultVerificationPolicy(JsonNode issuerNode, ObjectMapper mapper) throws URISyntaxException {
+ String issuerId =issuerNode.get("id").asText().strip();
+
+ return mapper.createObjectNode()
+ .put("type", "VerificationObject")
+ .put("allowedOrigins", getDefaultAllowedOrigins(issuerId))
+ .put("verificationProperty", "id");
+ }
+
+ private String getDefaultAllowedOrigins(String issuerId) throws URISyntaxException {
+ URI issuerUri = new URI(issuerId);
+ return issuerUri.getAuthority();
+ }
+
+ /**
+ * Return the ID of the node with name propertyName
+ * @param jsonNode node
+ * @return ID of the node. If node is textual, the text is returned. If node is an object, its "ID" attribute is returned
+ */
+ protected String getBadgeClaimId(JsonNode jsonNode) {
+ JsonNode propertyNode = jsonNode.get(propertyName);
+ if (propertyNode.isTextual()) {
+ return propertyNode.asText().strip();
+ }
+ return propertyNode.get("id").asText().strip();
+ }
+
+
+ public static final String ID = VerificationDependenciesProbe.class.getSimpleName();
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationJWTProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationJWTProbe.java
new file mode 100644
index 0000000..d342f31
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationJWTProbe.java
@@ -0,0 +1,114 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.KeyFactory;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.List;
+
+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.report.ReportItems;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+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;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.RSASSAVerifier;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+/**
+ * Recipient Verification probe for Open Badges 2.0
+ * Maps to "VERIFY_JWS" task in python implementation
+ * @author xaracil
+ */
+public class VerificationJWTProbe extends Probe {
+ final String jwt;
+
+ public VerificationJWTProbe(String jwt) {
+ super(ID);
+ this.jwt = jwt;
+ }
+
+ @Override
+ public ReportItems run(JsonLdGeneratedObject assertion, RunContext ctx) throws Exception {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode assertionNode = (mapper).readTree(assertion.getJson());
+ UriResourceFactory uriResourceFactory = (UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY);
+
+ // get badge from assertion
+ UriResource badgeUriResource = uriResourceFactory.of(assertionNode.get("badge").asText().strip());
+ JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(badgeUriResource));
+ JsonNode badgeNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(badgeObject.getJson());
+
+ // get issuer from badge
+ UriResource issuerUriResource = uriResourceFactory.of(badgeNode.get("issuer").asText().strip());
+
+ JsonLdGeneratedObject issuerObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(issuerUriResource));
+ JsonNode issuerNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(issuerObject.getJson());
+
+ // get verification from assertion
+ JsonNode creatorIdNode = assertionNode.get("verification").get("creator");
+ String creatorId = null;
+ if (creatorIdNode != null) {
+ creatorId = creatorIdNode.asText().strip();
+ } else {
+ // If not present, verifiers will check public key(s) declared in the referenced issuer Profile.
+ creatorId = issuerNode.get("publicKeyPem").asText().strip();
+ }
+
+ // get creator from id
+ UriResource creatorUriResource = uriResourceFactory.of(creatorId);
+ JsonLdGeneratedObject creatorObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
+ JsonLDCompactionProve.getId(creatorUriResource));
+ JsonNode creatorNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
+ .readTree(creatorObject.getJson());
+
+ // verify key ownership
+ String keyId = creatorNode.get("id").asText().strip();
+ List issuerKeys = JsonNodeUtil.asStringList(issuerNode.get("publicKey"));
+ if (!issuerKeys.contains(keyId)) {
+ return error("Assertion signed by a key " + keyId + " other than those authorized by issuer profile", ctx);
+ }
+ String publicKeyPem = creatorNode.get("publicKeyPem").asText().strip();
+
+ // verify signature
+ publicKeyPem = publicKeyPem.replace("-----BEGIN PUBLIC KEY-----", "");
+ publicKeyPem = publicKeyPem.replace("-----END PUBLIC KEY-----", "");
+ publicKeyPem = publicKeyPem.replace("\n", "");
+
+ byte[] encodedPb = Base64.getDecoder().decode(publicKeyPem);
+ X509EncodedKeySpec keySpecPb = new X509EncodedKeySpec(encodedPb);
+ RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpecPb);
+
+ JWSObject jwsObject = JWSObject.parse(jwt);
+ JWSVerifier verifier = new RSASSAVerifier(publicKey);
+ try {
+ if (!jwsObject.verify(verifier)) {
+ return error("Signature for node " + assertionNode.get("id") + " failed verification ", ctx);
+ }
+ } catch (JOSEException e) {
+ return fatal("Signature for node " + assertionNode.get("id") + " failed verification " + e.getLocalizedMessage(), ctx);
+ }
+ return success(ctx);
+ }
+
+ private static final List allowedTypes = List.of("id", "email", "url", "telephone");
+ public static final String ID = VerificationJWTProbe.class.getSimpleName();
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java
new file mode 100644
index 0000000..c6f4972
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java
@@ -0,0 +1,95 @@
+package org.oneedtech.inspect.vc.probe;
+
+import java.util.List;
+
+import org.bouncycastle.crypto.digests.GeneralDigest;
+import org.bouncycastle.crypto.digests.MD5Digest;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+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.probe.RunContext.Key;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.vc.Assertion;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Recipient Verification probe for Open Badges 2.0
+ * Maps to "VERIFY_RECIPIENT_IDENTIFIER" task in python implementation
+ * @author xaracil
+ */
+public class VerificationRecipientProbe extends Probe {
+ final String profileId;
+
+ public VerificationRecipientProbe(String profileId) {
+ super(ID);
+ this.profileId = profileId;
+ }
+
+ @Override
+ public ReportItems run(Assertion assertion, RunContext ctx) throws Exception {
+ ReportItems warnings = new ReportItems();
+ JsonNode recipientNode = assertion.getJson().get("recipient");
+
+ JsonLdGeneratedObject profileObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(profileId));
+ JsonNode profileNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER)).readTree(profileObject.getJson());
+
+ String type = recipientNode.get("type").asText().strip();
+ if (!allowedTypes.contains(type)) {
+ warnings = warning("Recipient identifier type " + type + " in assertion " + assertion.getJson().toString() + " is not one of the recommended types", ctx);
+ }
+
+ JsonNode typeNode = profileNode.get(type);
+ if (JsonNodeUtil.isEmpty(typeNode)) {
+ return new ReportItems(List.of(warnings, error("Profile identifier property of type " + typeNode + " not found in submitted profile " + profileId, ctx)));
+ }
+
+ JsonNode hashNode = recipientNode.get("hashed");
+ List currentTypes = JsonNodeUtil.asStringList(typeNode);
+ String identity = recipientNode.get("identity").asText().strip().toLowerCase();
+ String confirmedId = null;
+ if (JsonNodeUtil.isNotEmpty(hashNode) && hashNode.asBoolean()) {
+ String salt = recipientNode.get("salt").asText().strip();
+ for (String possibleId : currentTypes) {
+ if (hashMatch(possibleId, identity, salt)) {
+ confirmedId = possibleId;
+ break;
+ }
+ }
+ if (confirmedId == null) {
+ return new ReportItems(List.of(warnings, error("Profile " + profileId + " identifier(s) " + currentTypes + " of type " + typeNode.toString() + " did not match assertion " + assertion.getId() + " recipient hash " + identity + ".", ctx)));
+ }
+ } else if (currentTypes.contains(identity)) {
+ confirmedId = identity;
+ } else {
+ return new ReportItems(List.of(warnings, error("Profile " + profileId + " identifier " + currentTypes + " of type " + typeNode.toString() + " did not match assertion " + assertion.getId() + " recipient value " + identity, ctx)));
+ }
+
+ return new ReportItems(List.of(warnings, success(ctx)));
+ }
+
+ private boolean hashMatch(String possibleId, String identity, String salt) throws Exception {
+ String text = possibleId + salt;
+ GeneralDigest digest = null;
+ if (identity.startsWith("md5")) {
+ digest = new MD5Digest();
+ } else if (identity.startsWith("sha256")) {
+ digest = new SHA256Digest();
+ } else {
+ throw new IllegalAccessException("Cannot interpret hash type of " + identity);
+ }
+ digest.update(text.getBytes(), 0, text.length());
+ byte[] digested = new byte[digest.getDigestSize()];
+ digest.doFinal(digested, 0);
+ return new String(Hex.encode(digested)).equals(identity);
+ }
+
+ private static final List allowedTypes = List.of("id", "email", "url", "telephone");
+ public static final String ID = VerificationRecipientProbe.class.getSimpleName();
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java
new file mode 100644
index 0000000..30d3432
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java
@@ -0,0 +1,75 @@
+package org.oneedtech.inspect.vc.probe.validation;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.probe.RunContext.Key;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.util.resource.MimeType;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.Validation;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Image validation for Open Badges 2.0
+ * Maps to "IMAGE_VALIDATION" task in python implementation
+ * @author xaracil
+ */
+public class ValidationImagePropertyProbe extends ValidationPropertyProbe {
+
+ public ValidationImagePropertyProbe(String credentialType, Validation validation) {
+ this(credentialType, validation, true);
+ }
+
+ public ValidationImagePropertyProbe(String credentialType, Validation validation, boolean fullValidate) {
+ super(ID, credentialType, validation, fullValidate);
+ }
+
+ @Override
+ protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
+ // Could not load and validate image in node
+ return success(ctx);
+ }
+
+ @Override
+ protected ReportItems validate(JsonNode node, RunContext ctx) {
+ if (node.isArray()) {
+ return error("many images not allowed", ctx);
+ }
+ String url = node.isObject() ? node.get("id").asText() : node.asText();
+ if (PrimitiveValueValidator.validateDataUri(node)) {
+ if (!validation.isAllowDataUri()) {
+ return error("Image in node " + node + " may not be a data URI.", ctx);
+ }
+
+ // check mime types
+ final Pattern pattern = Pattern.compile("(^data):([^,]{0,}?)?(base64)?,(.*$)");
+ final Matcher matcher = pattern.matcher(url);
+ if (matcher.matches()) {
+ MimeType mimeType = new MimeType(matcher.toMatchResult().group(2));
+ if (!allowedMimeTypes.contains(mimeType)) {
+ return error("Data URI image does not declare any of the allowed PNG or SVG mime types in " + node.asText(), ctx);
+ }
+ }
+ } else if (!url.isEmpty()) {
+ try {
+ UriResource uriResource = ((UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY)).of(url);
+ // TODO: load resource from cache
+ // TODO: check accept type -> 'Accept': 'application/ld+json, application/json, image/png, image/svg+xml'
+ uriResource.asByteSource();
+ } catch (Throwable t) {
+ return fatal(t.getMessage(), ctx);
+ }
+ }
+ return success(ctx);
+ }
+
+ private static final List allowedMimeTypes = List.of(MimeType.IMAGE_PNG, MimeType.IMAGE_SVG);
+ public static final String ID = ValidationImagePropertyProbe.class.getSimpleName();
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java
new file mode 100644
index 0000000..8eeb931
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java
@@ -0,0 +1,45 @@
+package org.oneedtech.inspect.vc.probe.validation;
+
+import java.util.regex.Pattern;
+
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.vc.Validation;
+import org.oneedtech.inspect.vc.Validation.MessageLevel;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Issuer properties additional validator for Open Badges 2.0
+ * Maps to "ISSUER_PROPERTY_DEPENDENCIES" task in python implementation
+ * @author xaracil
+ */
+public class ValidationIssuerPropertyProbe extends ValidationPropertyProbe {
+
+ public ValidationIssuerPropertyProbe(String credentialType, Validation validation) {
+ this(credentialType, validation, true);
+ }
+
+ public ValidationIssuerPropertyProbe(String credentialType, Validation validation, boolean fullValidate) {
+ super(ID, credentialType, validation, fullValidate);
+ }
+
+ @Override
+ protected ReportItems validate(JsonNode node, RunContext ctx) {
+ if (!Pattern.compile("^http(s)?://.+", Pattern.CASE_INSENSITIVE).matcher(node.asText()).matches()) {
+ return buildResponse("Issuer Profile " + node.toString() + " not hosted with HTTP-based identifier." +
+ "Many platforms can only handle HTTP(s)-hosted issuers.", ctx);
+ }
+ return success(ctx);
+ }
+
+ private ReportItems buildResponse(String msg, RunContext ctx) {
+ if (validation.getMessageLevel() == MessageLevel.Warning) {
+ return warning(msg, ctx);
+ }
+ return error(msg, ctx);
+ }
+
+ public static final String ID = ValidationIssuerPropertyProbe.class.getSimpleName();
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java
new file mode 100644
index 0000000..d3590f2
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java
@@ -0,0 +1,188 @@
+package org.oneedtech.inspect.vc.probe.validation;
+
+import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI;
+import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI_OR_URL;
+import static org.oneedtech.inspect.vc.Assertion.ValueType.URL;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+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.report.ReportItems;
+import org.oneedtech.inspect.core.report.ReportUtil;
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.Assertion.ValueType;
+import org.oneedtech.inspect.vc.Validation;
+import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
+import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
+import org.oneedtech.inspect.vc.probe.PropertyProbe;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+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;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+/**
+ * Validator for properties of type other than ValueType.RDF_TYPE in Open Badges 2.0 types
+ * Maps to "VALIDATE_TYPE_PROPERTY" task in python implementation
+ * @author xaracil
+ */
+public class ValidationPropertyProbe extends PropertyProbe {
+ protected final Validation validation;
+ protected final boolean fullValidate;
+
+ public ValidationPropertyProbe(String credentialType, Validation validation) {
+ this(ID, credentialType, validation, true);
+ }
+
+ public ValidationPropertyProbe(String id, String credentialType, Validation validation) {
+ this(id, credentialType, validation, true);
+ }
+
+ public ValidationPropertyProbe(String credentialType, Validation validation, boolean fullValidate) {
+ this(ID, credentialType, validation, fullValidate);
+ }
+
+ public ValidationPropertyProbe(String id, String credentialType, Validation validation, boolean fullValidate) {
+ super(id, credentialType, validation.getName());
+ this.validation = validation;
+ this.fullValidate = fullValidate;
+ setValidations(this::validate);
+ }
+
+ @Override
+ protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
+ if (fullValidate && validation.isRequired()) {
+ return error("Required property " + validation.getName() + " not present in " + node.toPrettyString(), ctx);
+ } else {
+ // optional property or not doing full validation
+ return success(ctx);
+ }
+ }
+
+ /**
+ * Validates presence and data type of a single property that is
+ * expected to be one of the Open Badges Primitive data types or an ID.
+ * @param node node to check data type
+ * @param ctx associated run context
+ * @return validation result
+ */
+ protected ReportItems validate(JsonNode node, RunContext ctx) {
+ ReportItems result = new ReportItems();
+
+ // required property
+ if (validation.isRequired()) {
+ if (node.isObject()) {
+ if (!node.fieldNames().hasNext()) {
+ return error("Required property " + validation.getName() + " value " + node.toString() + " is not acceptable", ctx);
+ }
+ } else {
+ List values = JsonNodeUtil.asStringList(node);
+ if (values == null ||values.isEmpty()) {
+ return error("Required property " + validation.getName() + " value " + values + " is not acceptable", ctx);
+ }
+ }
+ }
+
+ List nodeList = JsonNodeUtil.asNodeList(node);
+ // many property
+ if (!validation.isMany()) {
+ if (nodeList.size() > 1) {
+ return error("Property " + validation.getName() + "has more than the single allowed value.", ctx);
+ }
+ }
+
+ try {
+ if (validation.getType() != ValueType.ID) {
+ Function validationFunction = validation.getType().getValidationFunction();
+ for (JsonNode childNode : nodeList) {
+ Boolean valid = validationFunction.apply(childNode);
+ if (!valid) {
+ return error(validation.getType() + " property " + validation.getName() + " value " + childNode.toString() + " not valid", ctx);
+ }
+ }
+ } else {
+ // pre-requisites
+ result = new ReportItems(List.of(result, validatePrerequisites(node, ctx)));
+ if (result.contains(Outcome.ERROR, Outcome.EXCEPTION)) {
+ return result;
+ }
+ for (JsonNode childNode : nodeList) {
+ if (childNode.isObject()) {
+ result = new ReportItems(List.of(result, validateExpectedTypes(childNode, ctx)));
+ continue;
+ } else if (validation.isAllowDataUri() && !DATA_URI_OR_URL.getValidationFunction().apply(childNode)){
+ return error("ID-type property " + validation.getName() + " had value `" + childNode.toString() + "` that isn't URI or DATA URI in " + node.toString(), ctx);
+ } else if (!validation.isAllowDataUri() && !ValueType.IRI.getValidationFunction().apply(childNode)) {
+ return error("ID-type property " + validation.getName() + " had value `" + childNode.toString() + "` where another scheme may have been expected " + node.toString(), ctx);
+ }
+
+ // get node from context
+ UriResource uriResource = ((UriResourceFactory) ctx.get(Key.URI_RESOURCE_FACTORY)).of(childNode.asText().strip());
+ JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
+ if (resolved == null) {
+ if (validation.isAllowRemoteUrl() && URL.getValidationFunction().apply(childNode)) {
+ continue;
+ }
+
+ if (validation.isAllowDataUri() && DATA_URI.getValidationFunction().apply(childNode)) {
+ continue;
+ }
+ return error("Node " + node.toString() + " has " + validation.getName() +" property value `" + childNode.toString() + "` that appears not to be in URI format", ctx);
+ } else {
+ ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
+ JsonNode resolvedNode = mapper.readTree(resolved.getJson());
+
+ // validate expected node class
+ result = new ReportItems(List.of(result, validateExpectedTypes(resolvedNode, ctx)));
+ }
+ }
+ }
+ } catch (Throwable t) {
+ return fatal(t.getMessage(), ctx);
+ }
+
+ return result.size() > 0 ? result : success(ctx);
+ }
+
+ private ReportItems validatePrerequisites(JsonNode node, RunContext ctx) {
+ List results = validation.getPrerequisites().stream()
+ .map(v -> ValidationPropertyProbeFactory.of(validation.getName(), v, validation.isFullValidate()))
+ .map(probe -> {
+ try {
+ return probe.run(node, ctx);
+ } catch (Exception e) {
+ return ReportUtil.onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, null, e);
+ }
+ })
+ .collect(Collectors.toList());
+
+ return new ReportItems(results);
+ }
+
+ private ReportItems validateExpectedTypes(JsonNode node, RunContext ctx) {
+ List results = validation.getExpectedTypes().stream()
+ .flatMap(type -> type.getValidations().stream())
+ .map(v -> ValidationPropertyProbeFactory.of(validation.getName(), v, validation.isFullValidate()))
+ .map(probe -> {
+ try {
+ return probe.run(node, ctx);
+ } catch (Exception e) {
+ return ReportUtil.onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, null, e);
+ }
+ })
+ .collect(Collectors.toList());
+ return new ReportItems(results);
+ }
+
+ public static final String ID = ValidationPropertyProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbeFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbeFactory.java
new file mode 100644
index 0000000..917ef2b
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbeFactory.java
@@ -0,0 +1,32 @@
+package org.oneedtech.inspect.vc.probe.validation;
+
+import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
+
+import org.oneedtech.inspect.vc.Assertion.ValueType;
+import org.oneedtech.inspect.vc.Credential.CredentialEnum;
+import org.oneedtech.inspect.vc.Assertion;
+import org.oneedtech.inspect.vc.Validation;
+
+/**
+ * Factory for ValidationPropertyProbes
+ * @author xaracil
+ */
+public class ValidationPropertyProbeFactory {
+ public static ValidationPropertyProbe of(String type, Validation validation) {
+ return of(type, validation, true);
+ }
+
+ public static ValidationPropertyProbe of(String type, Validation validation, boolean fullValidate) {
+ checkNotNull(validation.getType());
+ if (validation.getType() == ValueType.RDF_TYPE) {
+ return new ValidationRdfTypePropertyProbe(type, validation, fullValidate);
+ }
+ if (validation.getType() == ValueType.IMAGE) {
+ return new ValidationImagePropertyProbe(type, validation);
+ }
+ if (validation.getType() == ValueType.ISSUER) {
+ return new ValidationIssuerPropertyProbe(type, validation);
+ }
+ return new ValidationPropertyProbe(type, validation, fullValidate);
+ }
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java
new file mode 100644
index 0000000..5443752
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java
@@ -0,0 +1,69 @@
+package org.oneedtech.inspect.vc.probe.validation;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.oneedtech.inspect.core.probe.Outcome;
+import org.oneedtech.inspect.core.probe.RunContext;
+import org.oneedtech.inspect.core.report.ReportItems;
+import org.oneedtech.inspect.vc.Validation;
+import org.oneedtech.inspect.vc.util.JsonNodeUtil;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.TextNode;
+
+/**
+ * Validator for properties of type ValueType.RDF_TYPE in Open Badges 2.0 types
+ * Maps to "VALIDATE_RDF_TYPE_PROPERTY" task in python implementation
+ * @author xaracil
+ */
+public class ValidationRdfTypePropertyProbe extends ValidationPropertyProbe {
+ public ValidationRdfTypePropertyProbe(String credentialType, Validation validation ) {
+ this(credentialType, validation, true);
+ }
+
+ public ValidationRdfTypePropertyProbe(String credentialType, Validation validation, boolean fullValidate) {
+ super(ID, credentialType, validation, fullValidate);
+ }
+
+ @Override
+ protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
+ if (!validation.isRequired()) {
+ // check if we have a default type
+ if (validation.getDefaultType() != null) {
+ JsonNodeFactory factory = JsonNodeFactory.instance;
+ TextNode textNode = factory.textNode(validation.getDefaultType());
+ // validate with default value
+ return validate(textNode, ctx);
+ }
+ }
+
+ // if we're not doing a full validation and it's not and id field, pass
+ if (!fullValidate && !validation.getName().equals("id")) {
+ return success(ctx);
+ }
+ return error("Required property " + validation.getName() + " not present in " + node.toPrettyString(), ctx);
+ }
+
+ @Override
+ protected ReportItems validate(JsonNode node, RunContext ctx) {
+ ReportItems result = super.validate(node, ctx);
+ if (result.contains(Outcome.ERROR, Outcome.FATAL)) {
+ return result;
+ }
+ if (!validation.getMustContainOne().isEmpty()) {
+ List values = JsonNodeUtil.asStringList(node);
+ boolean valid = validation.getMustContainOne().stream().anyMatch(type -> values.contains(type));
+ if (!valid) {
+ return new ReportItems(List.of(result,
+ fatal("Node " + validation.getName() + " of type " + node.asText()
+ + " does not have type among allowed values (" + validation.getMustContainOne().stream().collect(Collectors.joining(",")) + ")", ctx)
+ ));
+ }
+ }
+ return new ReportItems(List.of(result, success(ctx)));
+ }
+
+ public static final String ID = ValidationRdfTypePropertyProbe.class.getSimpleName();
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/DefaultUriResourceFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/DefaultUriResourceFactory.java
new file mode 100644
index 0000000..2c117d1
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/DefaultUriResourceFactory.java
@@ -0,0 +1,19 @@
+package org.oneedtech.inspect.vc.resource;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.oneedtech.inspect.util.resource.UriResource;
+
+/**
+ * Default factory for URIResources
+ * @author xaracil
+ */
+public class DefaultUriResourceFactory implements UriResourceFactory {
+
+ @Override
+ public UriResource of(String uri) throws URISyntaxException {
+ return new UriResource(new URI(uri));
+ }
+
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/UriResourceFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/UriResourceFactory.java
new file mode 100644
index 0000000..b5861f4
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/resource/UriResourceFactory.java
@@ -0,0 +1,13 @@
+package org.oneedtech.inspect.vc.resource;
+
+import java.net.URISyntaxException;
+
+import org.oneedtech.inspect.util.resource.UriResource;
+
+/**
+ * Factory interface for URI resources
+ * @author xaracil
+ */
+public interface UriResourceFactory {
+ public UriResource of(String uri) throws URISyntaxException;
+}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
index 6db88fc..a4ba642 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/CachingDocumentLoader.java
@@ -2,8 +2,12 @@ package org.oneedtech.inspect.vc.util;
import java.io.InputStream;
import java.net.URI;
+import java.net.URISyntaxException;
import java.net.URL;
import java.time.Duration;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -21,21 +25,84 @@ import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
/**
* A com.apicatalog DocumentLoader with a threadsafe static cache.
*
* @author mgylling
*/
-public class CachingDocumentLoader implements DocumentLoader {
+public class CachingDocumentLoader extends ConfigurableDocumentLoader {
+ private Set contexts;
+
+ public CachingDocumentLoader() {
+ this(null);
+ }
+
+ public CachingDocumentLoader(Map localDomains) {
+ super();
+ setEnableHttp(true);
+ setEnableHttps(true);
+ setDefaultHttpLoader(new HttpLoader(localDomains));
+ this.contexts = new HashSet<>();
+ }
@Override
public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError {
- Tuple tpl = new Tuple<>(url.toASCIIString(), options);
- try {
- return documentCache.get(tpl);
- } catch (Exception e) {
+ Document document = super.loadDocument(url, options);
+ if (document == null) {
logger.error("documentCache not able to load {}", url);
- throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage());
+ throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT);
+ }
+ // save context
+ this.contexts.add(url);
+
+ return document;
+ }
+
+ public Set getContexts() {
+ return contexts;
+ }
+
+ public void resetContexts() {
+ this.contexts.clear();
+ }
+
+ public class HttpLoader implements DocumentLoader {
+ final Map localDomains;
+
+ public HttpLoader(Map localDomains) {
+ this.localDomains = localDomains;
+ }
+
+ @Override
+ public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError {
+ try {
+ // resolve url
+ URI resolvedUrl = resolve(url);
+
+ Tuple tpl = new Tuple<>(resolvedUrl.toASCIIString(), options);
+
+ return documentCache.get(tpl);
+ } catch (Exception e) {
+ logger.error("documentCache not able to load {}", url);
+ throw new JsonLdError(JsonLdErrorCode.INVALID_REMOTE_CONTEXT, e.getMessage());
+ }
+ }
+
+ /**
+ * Resolved given url. If the url is from one of local domain, a URL of the relative resource will be returned
+ * @throws URISyntaxException
+ */
+ public URI resolve(URI url) throws URISyntaxException {
+ if (localDomains != null) {
+ URI base = url.resolve("/");
+ if (localDomains.containsKey(base)) {
+ URL resource = Resources.getResource(localDomains.get(base) + "/" + base.relativize(url).toString());
+ return resource.toURI();
+ }
+ }
+ return url;
}
}
@@ -55,6 +122,12 @@ public class CachingDocumentLoader implements DocumentLoader {
.put("https://w3id.org/security/suites/ed25519-2018/v1", Resources.getResource("contexts/suites-ed25519-2018.jsonld"))
.put("https://w3id.org/security/suites/x25519-2019/v1", Resources.getResource("contexts/suites-x25519-2019.jsonld"))
.put("https://w3id.org/security/suites/jws-2020/v1", Resources.getResource("contexts/suites-jws-2020.jsonld"))
+ .put("https://openbadgespec.org/v2/context.json", Resources.getResource("contexts/ob-v2p0.json"))
+ .put("https://w3id.org/openbadges/v2", Resources.getResource("contexts/obv2x.jsonld"))
+ .put("https://w3id.org/openbadges/extensions/exampleExtension/context.json", Resources.getResource("contexts/obv2x-extensions.json"))
+ .put("https://openbadgespec.org/extensions/exampleExtension/schema.json", Resources.getResource("catalog/openbadgespec.org/extensions/exampleExtension/schema.json"))
+ .put("https://w3id.org/openbadges/extensions/applyLinkExtension/context.json", Resources.getResource("contexts/obv2x-applylink-extensions.json"))
+ .put("https://openbadgespec.org/extensions/applyLinkExtension/schema.json", Resources.getResource("catalog/openbadgespec.org/extensions/applyLinkExtension/schema.json"))
.build();
@@ -62,7 +135,8 @@ public class CachingDocumentLoader implements DocumentLoader {
.initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24))
.build(new CacheLoader, Document>() {
public Document load(final Tuple id) throws Exception {
- try (InputStream is = bundled.keySet().contains(id.t1)
+ // TODO: this loading will fail if the document is redirected (HTTP 301)
+ try (InputStream is = bundled.containsKey(id.t1)
? bundled.get(id.t1).openStream()
: new URI(id.t1).toURL().openStream();) {
return JsonDocument.of(is);
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java
index aaf6083..080bc6f 100644
--- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java
@@ -17,11 +17,11 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
public class JsonNodeUtil {
public static List asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) {
- List list = new ArrayList<>();
- ArrayNode array = evaluator.eval(jsonPath, root);
+ List list = new ArrayList<>();
+ ArrayNode array = evaluator.eval(jsonPath, root);
for(JsonNode node : array) {
if(!(node instanceof ArrayNode)) {
- list.add(node);
+ list.add(node);
} else {
ArrayNode values = (ArrayNode) node;
for(JsonNode value : values) {
@@ -31,28 +31,39 @@ public class JsonNodeUtil {
}
return list;
}
-
+
public static List asStringList(JsonNode node) {
if(!(node instanceof ArrayNode)) {
+ if (node.isObject()) {
+ return List.of();
+ }
return List.of(node.asText());
} else {
- ArrayNode arrayNode = (ArrayNode)node;
+ ArrayNode arrayNode = (ArrayNode)node;
return StreamSupport
.stream(arrayNode.spliterator(), false)
.map(n->n.asText().strip())
- .collect(Collectors.toList());
+ .collect(Collectors.toList());
}
}
-
+
public static List asNodeList(JsonNode node) {
if(node == null) return null;
if(!(node instanceof ArrayNode)) {
return List.of(node);
} else {
- ArrayNode arrayNode = (ArrayNode)node;
+ ArrayNode arrayNode = (ArrayNode)node;
return StreamSupport
.stream(arrayNode.spliterator(), false)
- .collect(Collectors.toList());
+ .collect(Collectors.toList());
}
}
+
+ public static boolean isNotEmpty(JsonNode node) {
+ return node != null && !node.isNull() && !node.isEmpty();
+ }
+
+ public static boolean isEmpty(JsonNode node) {
+ return node == null || node.isNull() || node.isEmpty();
+ }
}
diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidator.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidator.java
new file mode 100644
index 0000000..e42453f
--- /dev/null
+++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidator.java
@@ -0,0 +1,212 @@
+package org.oneedtech.inspect.vc.util;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.time.chrono.IsoChronology;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.IllformedLocaleException;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.util.json.ObjectMapperCache.Config;
+
+import com.apicatalog.jsonld.JsonLd;
+import com.apicatalog.jsonld.JsonLdError;
+import com.apicatalog.jsonld.document.JsonDocument;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.google.common.io.Resources;
+
+/**
+ * Validator for ValueType. Translated into java from PrimitiveValueValidator in validation.py
+ */
+public class PrimitiveValueValidator {
+
+ public static boolean validateBoolean(JsonNode value) {
+ return value.isValueNode() && value.isBoolean();
+ }
+
+ public static boolean validateCompactIri(JsonNode value) {
+ if (value.asText().equals("id") || validateIri(value)) {
+ return true;
+ }
+
+ ObjectMapper mapper = ObjectMapperCache.get(Config.DEFAULT); // TODO: get from RunContext
+
+ try {
+ JsonNode node = mapper.readTree(Resources.getResource("contexts/ob-v2p0.json"));
+ ObjectReader readerForUpdating = mapper.readerForUpdating(node);
+ JsonNode merged = readerForUpdating.readValue("{\"" + value.asText() + "\" : \"TEST\"}");
+ JsonDocument jsonDocument = JsonDocument.of(new StringReader(merged.toString()));
+
+ JsonNode expanded = mapper.readTree(JsonLd.expand(jsonDocument).get().toString());
+ if (expanded.isArray() && ((ArrayNode) expanded).size() > 0) {
+ return true;
+ }
+
+ } catch (NullPointerException | IOException | JsonLdError e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ public static boolean validateDataUri(JsonNode value) {
+ try {
+ URI uri = new URI(value.asText());
+ return "data".equalsIgnoreCase(uri.getScheme()) && uri.getSchemeSpecificPart().contains(",");
+ } catch (Throwable ignored) {
+ }
+ return false;
+ }
+
+ public static boolean validateDataUriOrUrl(JsonNode value) {
+ return validateUrl(value) || validateDataUri(value);
+ }
+
+ private static DateTimeFormatter ISO_OFFSET_TIME_JOINED = new DateTimeFormatterBuilder()
+ .parseCaseInsensitive()
+ .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
+ .parseLenient()
+ .appendOffset("+Hmmss", "Z")
+ .parseStrict()
+ .toFormatter();
+
+ public static boolean validateDatetime(JsonNode value) {
+ boolean valid = List.of(ISO_OFFSET_TIME_JOINED,
+ DateTimeFormatter.ISO_OFFSET_DATE_TIME,
+ DateTimeFormatter.ISO_INSTANT)
+ .stream().anyMatch(formatter -> {
+ try {
+ formatter.parse(value.asText());
+ return true;
+ } catch (DateTimeParseException | NullPointerException ignored) {
+ return false;
+ }
+ });
+
+ return valid;
+ }
+
+ public static boolean validateEmail(JsonNode value) {
+ return value.asText().matches("(^[^@\\s]+@[^@\\s]+$)");
+ }
+
+ public static boolean is_hashed_identity_hash(JsonNode value) {
+ return value.asText().matches("md5\\$[\\da-fA-F]{32}$") || value.asText().matches("sha256\\$[\\da-fA-F]{64}$");
+ }
+
+ /**
+ * Validates that identity is a string. More specific rules may only be enforced at the class instance level.
+ * @param value
+ * @return
+ */
+ public static boolean validateIdentityHash(JsonNode value) {
+ return validateText(value);
+ }
+
+ /**
+ * Checks if a string matches an acceptable IRI format and scheme. For now, only accepts a few schemes,
+ * 'http', 'https', blank node identifiers, and 'urn:uuid'
+
+ * @return
+ */
+ public static boolean validateIri(JsonNode value) {
+ return
+ Pattern.compile("^_:.+", Pattern.CASE_INSENSITIVE).matcher(value.asText()).matches()
+ || Pattern.compile("^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE).matcher(value.asText()).matches()
+ || validateUrl(value);
+ }
+
+ public static boolean validateLanguage(JsonNode value) {
+ try {
+ return validateText(value) && new Locale.Builder().setLanguageTag(value.asText()).build() != null;
+ } catch (IllformedLocaleException ignored) {
+ // value is not a valid locale
+ }
+ return false;
+ }
+
+ public static boolean validateMarkdown(JsonNode value) {
+ return validateText(value);
+ }
+
+ public static boolean validateRdfType(JsonNode value) {
+ if (!validateText(value)) {
+ return false;
+ }
+
+ ObjectMapper mapper = ObjectMapperCache.get(Config.DEFAULT); // TODO: get from RunContext
+ JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); // TODO: get from RunContext
+
+ try {
+ JsonNode node = mapper.readTree(Resources.getResource("contexts/ob-v2p0.json"));
+ ObjectReader readerForUpdating = mapper.readerForUpdating(node);
+ JsonNode merged = readerForUpdating.readValue("{\"type\": \"" + value.asText() + "\"}");
+
+ JsonDocument jsonDocument = JsonDocument.of(new StringReader(merged.toString()));
+ JsonNode expanded = mapper.readTree(JsonLd.expand(jsonDocument).get().toString());
+
+ return validateIri(JsonNodeUtil.asNodeList(expanded, "$[0].@type[0]", jsonPath).get(0));
+
+ } catch (NullPointerException | IOException | JsonLdError e) {
+ return false;
+ }
+ }
+
+ public static boolean validateTelephone(JsonNode value) {
+ return value.asText().matches("^\\+?[1-9]\\d{1,14}(;ext=\\d+)?$");
+ }
+
+ public static boolean validateText(JsonNode value) {
+ return value.isValueNode() && value.isTextual();
+ }
+
+ public static boolean validateTextOrNumber(JsonNode value) {
+ return value.isValueNode() && value.isTextual() || value.isNumber();
+ }
+
+ public static boolean validateUrl(JsonNode value) {
+ if (!value.isValueNode()) {
+ return false;
+ }
+
+ try {
+ new URL(value.asText());
+ return true;
+ } catch (MalformedURLException ignored) {
+ // value is not a valid URL
+ }
+ return false;
+ }
+
+ public static boolean validateUrlAuthority(JsonNode value) {
+ if (!validateText(value)) {
+ return false;
+ }
+
+ URI testUri;
+ try {
+ testUri = new URI("http://" + value.asText() + "/test");
+ String host = testUri.getHost();
+ if (host == null || !host.matches("(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\\.)+[a-zA-Z]{2,63}$)")) {
+ return false;
+ }
+ return testUri.getScheme().equals("http") && host.equals(value.asText()) && testUri.getPath().equals("/test") && testUri.getQuery() == null;
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/ob-v2p0.json b/inspector-vc/src/main/resources/contexts/ob-v2p0.json
new file mode 100644
index 0000000..84c3f37
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/ob-v2p0.json
@@ -0,0 +1,91 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+
+ "author": { "@id": "schema:author", "@type": "@id" },
+ "caption": { "@id": "schema:caption" },
+ "claim": {"@id": "cred:claim", "@type": "@id"},
+ "created": { "@id": "dc:created", "@type": "xsd:dateTime" },
+ "creator": { "@id": "dc:creator", "@type": "@id" },
+ "description": { "@id": "schema:description" },
+ "email": { "@id": "schema:email" },
+ "endorsement": {"@id": "cred:credential", "@type": "@id"},
+ "expires": { "@id": "sec:expiration", "@type": "xsd:dateTime" },
+ "genre": { "@id": "schema:genre" },
+ "image": { "@id": "schema:image", "@type": "@id" },
+ "name": { "@id": "schema:name" },
+ "owner": {"@id": "sec:owner", "@type": "@id"},
+ "publicKey": { "@id": "sec:publicKey", "@type": "@id" },
+ "publicKeyPem": { "@id": "sec:publicKeyPem" },
+ "related": { "@id": "dc:relation", "@type": "@id" },
+ "startsWith": { "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith" },
+ "tags": { "@id": "schema:keywords" },
+ "targetDescription": { "@id": "schema:targetDescription" },
+ "targetFramework": { "@id": "schema:targetFramework" },
+ "targetName": { "@id": "schema:targetName" },
+ "targetUrl": { "@id": "schema:targetUrl" },
+ "telephone": { "@id": "schema:telephone" },
+ "url": { "@id": "schema:url", "@type": "@id" },
+ "version": { "@id": "schema:version" },
+
+ "alignment": { "@id": "obi:alignment", "@type": "@id" },
+ "allowedOrigins": { "@id": "obi:allowedOrigins" },
+ "audience": { "@id": "obi:audience" },
+ "badge": { "@id": "obi:badge", "@type": "@id" },
+ "criteria": { "@id": "obi:criteria", "@type": "@id" },
+ "endorsementComment": { "@id": "obi:endorsementComment" },
+ "evidence": { "@id": "obi:evidence", "@type": "@id" },
+ "hashed": { "@id": "obi:hashed", "@type": "xsd:boolean" },
+ "identity": { "@id": "obi:identityHash" },
+ "issuedOn": { "@id": "obi:issueDate", "@type": "xsd:dateTime" },
+ "issuer": { "@id": "obi:issuer", "@type": "@id" },
+ "narrative": { "@id": "obi:narrative" },
+ "recipient": { "@id": "obi:recipient", "@type": "@id" },
+ "revocationList": { "@id": "obi:revocationList", "@type": "@id" },
+ "revocationReason": { "@id": "obi:revocationReason" },
+ "revoked": { "@id": "obi:revoked", "@type": "xsd:boolean" },
+ "revokedAssertions": { "@id": "obi:revoked" },
+ "salt": { "@id": "obi:salt" },
+ "targetCode": { "@id": "obi:targetCode" },
+ "uid": { "@id": "obi:uid" },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": { "@id": "obi:verify", "@type": "@id" },
+ "verificationProperty": { "@id": "obi:verificationProperty" },
+ "verify": "verification"
+ }
+}
diff --git a/inspector-vc/src/main/resources/contexts/obv2x-applylink-extensions.json b/inspector-vc/src/main/resources/contexts/obv2x-applylink-extensions.json
new file mode 100644
index 0000000..da70560
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/obv2x-applylink-extensions.json
@@ -0,0 +1,13 @@
+{
+ "@context": {
+ "obi": "https://w3id.org/openbadges#",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "url": "extensions:applyLink"
+ },
+ "obi:validation": [
+ {
+ "obi:validatesType": "extensions:ApplyLink",
+ "obi:validationSchema": "https://openbadgespec.org/extensions/applyLinkExtension/schema.json"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/obv2x-extensions.json b/inspector-vc/src/main/resources/contexts/obv2x-extensions.json
new file mode 100644
index 0000000..d759111
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/obv2x-extensions.json
@@ -0,0 +1,12 @@
+{
+ "@context": {
+ "obi": "https://w3id.org/openbadges#",
+ "exampleProperty": "http://schema.org/text"
+ },
+ "obi:validation": [
+ {
+ "obi:validatesType": "obi:extensions/#ExampleExtension",
+ "obi:validationSchema": "https://openbadgespec.org/extensions/exampleExtension/schema.json"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/main/resources/contexts/obv2x.jsonld b/inspector-vc/src/main/resources/contexts/obv2x.jsonld
new file mode 100644
index 0000000..84c3f37
--- /dev/null
+++ b/inspector-vc/src/main/resources/contexts/obv2x.jsonld
@@ -0,0 +1,91 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+
+ "author": { "@id": "schema:author", "@type": "@id" },
+ "caption": { "@id": "schema:caption" },
+ "claim": {"@id": "cred:claim", "@type": "@id"},
+ "created": { "@id": "dc:created", "@type": "xsd:dateTime" },
+ "creator": { "@id": "dc:creator", "@type": "@id" },
+ "description": { "@id": "schema:description" },
+ "email": { "@id": "schema:email" },
+ "endorsement": {"@id": "cred:credential", "@type": "@id"},
+ "expires": { "@id": "sec:expiration", "@type": "xsd:dateTime" },
+ "genre": { "@id": "schema:genre" },
+ "image": { "@id": "schema:image", "@type": "@id" },
+ "name": { "@id": "schema:name" },
+ "owner": {"@id": "sec:owner", "@type": "@id"},
+ "publicKey": { "@id": "sec:publicKey", "@type": "@id" },
+ "publicKeyPem": { "@id": "sec:publicKeyPem" },
+ "related": { "@id": "dc:relation", "@type": "@id" },
+ "startsWith": { "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith" },
+ "tags": { "@id": "schema:keywords" },
+ "targetDescription": { "@id": "schema:targetDescription" },
+ "targetFramework": { "@id": "schema:targetFramework" },
+ "targetName": { "@id": "schema:targetName" },
+ "targetUrl": { "@id": "schema:targetUrl" },
+ "telephone": { "@id": "schema:telephone" },
+ "url": { "@id": "schema:url", "@type": "@id" },
+ "version": { "@id": "schema:version" },
+
+ "alignment": { "@id": "obi:alignment", "@type": "@id" },
+ "allowedOrigins": { "@id": "obi:allowedOrigins" },
+ "audience": { "@id": "obi:audience" },
+ "badge": { "@id": "obi:badge", "@type": "@id" },
+ "criteria": { "@id": "obi:criteria", "@type": "@id" },
+ "endorsementComment": { "@id": "obi:endorsementComment" },
+ "evidence": { "@id": "obi:evidence", "@type": "@id" },
+ "hashed": { "@id": "obi:hashed", "@type": "xsd:boolean" },
+ "identity": { "@id": "obi:identityHash" },
+ "issuedOn": { "@id": "obi:issueDate", "@type": "xsd:dateTime" },
+ "issuer": { "@id": "obi:issuer", "@type": "@id" },
+ "narrative": { "@id": "obi:narrative" },
+ "recipient": { "@id": "obi:recipient", "@type": "@id" },
+ "revocationList": { "@id": "obi:revocationList", "@type": "@id" },
+ "revocationReason": { "@id": "obi:revocationReason" },
+ "revoked": { "@id": "obi:revoked", "@type": "xsd:boolean" },
+ "revokedAssertions": { "@id": "obi:revoked" },
+ "salt": { "@id": "obi:salt" },
+ "targetCode": { "@id": "obi:targetCode" },
+ "uid": { "@id": "obi:uid" },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": { "@id": "obi:verify", "@type": "@id" },
+ "verificationProperty": { "@id": "obi:verificationProperty" },
+ "verify": "verification"
+ }
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB20Tests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB20Tests.java
new file mode 100644
index 0000000..2cf8d99
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/OB20Tests.java
@@ -0,0 +1,334 @@
+package org.oneedtech.inspect.vc;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.oneedtech.inspect.test.Assertions.assertFatalCount;
+import static org.oneedtech.inspect.test.Assertions.assertHasProbeID;
+import static org.oneedtech.inspect.test.Assertions.assertInvalid;
+import static org.oneedtech.inspect.test.Assertions.assertValid;
+import static org.oneedtech.inspect.test.Assertions.assertWarning;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Nested;
+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.ContextPropertyProbe;
+import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
+import org.oneedtech.inspect.vc.util.TestOB20Inspector.TestBuilder;
+
+public class OB20Tests {
+ private static OB20Inspector validator;
+ private static boolean verbose = true;
+
+ @BeforeAll
+ static void setup() throws URISyntaxException {
+ TestBuilder builder = new TestBuilder();
+ for (String localDomain : localDomains) {
+ builder.add(new URI(localDomain), "ob20/assets");
+ }
+ validator = builder
+ .set(Behavior.TEST_INCLUDE_SUCCESS, true)
+ .set(Behavior.TEST_INCLUDE_WARNINGS, false)
+ .set(Behavior.VALIDATOR_FAIL_FAST, true)
+ .set(OB20Inspector.Behavior.ALLOW_LOCAL_REDIRECTION, true)
+ .build();
+ }
+
+ @Test
+ void testSimpleJsonValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_ASSERTION_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimplePNGPlainValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.PNG.SIMPLE_JSON_PNG.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimpleBadgeClassJsonValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_BADGECLASS.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testSimpleJsonInvalidContext() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_ASSERTION_INVALID_CONTEXT_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertFatalCount(report, 1);
+ assertHasProbeID(report, ContextPropertyProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testSimpleJsonInvalidType() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_ASSERTION_INVALID_TYPE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ assertFatalCount(report, 1);
+ assertHasProbeID(report, TypePropertyProbe.ID, true);
+ });
+ }
+
+ @Test
+ void testSimpleJWTValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JWT.SIMPLE_JWT.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testJWTNotRevoked() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JWT.SIMPLE_NOT_REVOKED_JWT.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testJWTRevoked() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JWT.SIMPLE_REVOKED_JWT.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testDataImageInBadge() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.BADGE_WITH_DATA_IMAGE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testDataImageInAssertion() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ASSERTION_WITH_DATA_IMAGE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testComplexImageInAssertion() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.BADGE_WITH_COMPLEX_IMAGE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testEndorsementsInAssertion() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ASSERTION_WITH_ENDORSEMENTS.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testNoPublicKeyInIssuer() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_ASSERTION_ISSUER_WITHOUT_PUBLIC_KEY_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testCompactIriInIssuer() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_COMPACTIRI_VALIDATION.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testRdfValidation() {
+ List.of(Samples.OB20.JSON.RDF_VALIDATION_VALID_BADGE_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_VALID_ISSUER_EXTENSION_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_VALID_ALIGNMENT_OBJECT,
+ Samples.OB20.JSON.RDF_VALIDATION_VALID_EXTERNAL_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_VALID_EMPTY_CRITERIA_TYPE).forEach(resource -> {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(resource.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ });
+ List.of(Samples.OB20.JSON.RDF_VALIDATION_INVALID_EMPTY_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_INVALID_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_INVALID_ELEM_CLASS,
+ Samples.OB20.JSON.RDF_VALIDATION_INVALID_ISSUER_TYPE).forEach(resource -> {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(resource.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ });
+
+ }
+
+ @Test
+ void testVerification() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_WITH_ALLOWED_ORIGINS.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testVerificationStartsWith() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_WITH_ALLOWED_ORIGINS_VALID_STARTSWITH.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_WITH_ALLOWED_ORIGINS_INVALID_STARTSWITH.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testVerificationMultipleStartsWith() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_WITH_ALLOWED_ORIGINS_VALID_MULTIPLE_STARTSWITH.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ISSUER_WITH_ALLOWED_ORIGINS_INVALID_MULTIPLE_STARTSWITH.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testExpired() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_EXPIRED_ASSERTION_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testExpiredBeforeIssued() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_EXPIRED_BEFORE_ISSUED_ASSERTION_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testIssuedInFuture() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.SIMPLE_FUTURE_ASSERTION_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Test
+ void testAssertionWithLanguage() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.BASIC_WITH_LANGUAGE_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testExtensionNode() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ASSERTION_WITH_EXTENSION_NODE_BASIC_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testMultipleExtensionNode() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ASSERTION_WITH_MULTIPLE_EXTENSIONS_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertValid(report);
+ });
+ }
+
+ @Test
+ void testInvalidExtensionNode() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.ASSERTION_WITH_EXTENSION_NODE_INVALID_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertInvalid(report);
+ });
+ }
+
+ @Nested
+ static class WarningTests {
+ @BeforeAll
+ static void setup() throws URISyntaxException {
+ TestBuilder builder = new TestBuilder();
+ for (String localDomain : localDomains) {
+ builder.add(new URI(localDomain), "ob20/assets");
+ }
+ validator = builder
+ .set(Behavior.TEST_INCLUDE_SUCCESS, true)
+ .set(Behavior.TEST_INCLUDE_WARNINGS, true)
+ .set(Behavior.VALIDATOR_FAIL_FAST, true)
+ .set(OB20Inspector.Behavior.ALLOW_LOCAL_REDIRECTION, true)
+ .build();
+ }
+
+ @Test
+ void testWarningRedirectionJsonValid() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.WARNING_REDIRECTION_ASSERTION_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertWarning(report);
+ });
+ }
+
+ @Test
+ void testWarningIssuerNonHttps() {
+ assertDoesNotThrow(()->{
+ Report report = validator.run(Samples.OB20.JSON.WARNING_ISSUER_NON_HTTPS_JSON.asFileResource());
+ if(verbose) PrintHelper.print(report, true);
+ assertWarning(report);
+ });
+ }
+ }
+
+ private static final List localDomains = List.of("https://www.example.org/", "https://example.org/", "http://example.org/");
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
index f47703a..f95ce38 100644
--- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/Samples.java
@@ -3,41 +3,114 @@ package org.oneedtech.inspect.vc;
import org.oneedtech.inspect.test.Sample;
public class Samples {
- public static final class OB30 {
- public static final class SVG {
+ public static final class OB30 {
+ public static final class SVG {
public final static Sample SIMPLE_JSON_SVG = new Sample("ob30/simple-json.svg", true);
- public final static Sample SIMPLE_JWT_SVG = new Sample("ob30/simple-jwt.svg", true);
+ public final static Sample SIMPLE_JWT_SVG = new Sample("ob30/simple-jwt.svg", true);
}
- public static final class JSON {
+ public static final class JSON {
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_DID_METHOD_JSON = new Sample("ob30/simple-did-method.json", true);
public final static Sample SIMPLE_JSON_NOPROOF = new Sample("ob30/simple-noproof.json", false);
public final static Sample SIMPLE_JSON_UNKNOWN_TYPE = new Sample("ob30/simple-err-type.json", false);
- public final static Sample SIMPLE_JSON_PROOF_METHOD_ERROR = new Sample("ob30/simple-err-proof-method.json", false);
- public final static Sample SIMPLE_JSON_PROOF_METHOD_NO_SCHEME_ERROR = new Sample("ob30/simple-err-proof-method-no-scheme.json", false);
- public final static Sample SIMPLE_JSON_PROOF_METHOD_UNKNOWN_SCHEME_ERROR = new Sample("ob30/simple-err-proof-method-unknown-scheme.json", false);
- public final static Sample SIMPLE_JSON_PROOF_METHOD_UNKNOWN_DID_METHOD_ERROR = new Sample("ob30/simple-err-proof-method-unknown-did-method.json", false);
- public final static Sample SIMPLE_JSON_PROOF_VALUE_ERROR = new Sample("ob30/simple-err-proof-value.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_METHOD_ERROR = new Sample("ob30/simple-err-proof-method.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_METHOD_NO_SCHEME_ERROR = new Sample("ob30/simple-err-proof-method-no-scheme.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_METHOD_UNKNOWN_SCHEME_ERROR = new Sample("ob30/simple-err-proof-method-unknown-scheme.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_METHOD_UNKNOWN_DID_METHOD_ERROR = new Sample("ob30/simple-err-proof-method-unknown-did-method.json", false);
+ public final static Sample SIMPLE_JSON_PROOF_VALUE_ERROR = new Sample("ob30/simple-err-proof-value.json", false);
public final static Sample SIMPLE_JSON_EXPIRED = new Sample("ob30/simple-err-expired.json", false);
public final static Sample SIMPLE_JSON_ISSUED = new Sample("ob30/simple-err-issued.json", false);
public final static Sample SIMPLE_JSON_ISSUER = new Sample("ob30/simple-err-issuer.json", false);
public final static Sample SIMPLE_JSON_ERR_CONTEXT = new Sample("ob30/simple-err-context.json", false);
}
- public static final class PNG {
+ public static final class PNG {
public final static Sample SIMPLE_JWT_PNG = new Sample("ob30/simple-jwt.png", true);
- public final static Sample SIMPLE_JSON_PNG = new Sample("ob30/simple-json.png", true);
+ public final static Sample SIMPLE_JSON_PNG = new Sample("ob30/simple-json.png", true);
}
- public static final class JWT {
+ public static final class JWT {
public final static Sample SIMPLE_JWT = new Sample("ob30/simple.jwt", true);
}
}
- public static final class CLR20 {
- public static final class JSON {
+ public static final class CLR20 {
+ public static final class JSON {
public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true);
public final static Sample SIMPLE_JSON_NOPROOF = new Sample("clr20/simple-noproof.json", true);
public final static Sample SIMPLE_JWT = new Sample("clr20/simple.jwt", true);
}
}
+
+ public static final class OB20 {
+ public static final class JSON {
+ // original: test_verify: test_verify_function
+ public final static Sample SIMPLE_ASSERTION_JSON = new Sample("ob20/basic-assertion.json", true);
+ public final static Sample SIMPLE_ASSERTION_INVALID_CONTEXT_JSON = new Sample("ob20/basic-assertion-invalid-context.json", true);
+ public final static Sample SIMPLE_ASSERTION_INVALID_TYPE_JSON = new Sample("ob20/basic-assertion-invalid-type.json", true);
+ // original:
+ public final static Sample SIMPLE_ASSERTION_ISSUER_WITHOUT_PUBLIC_KEY_JSON = new Sample("ob20/basic-assertion-no-public-key.json", true);
+ // original: test_graph: test_verify_with_redirection
+ public final static Sample WARNING_REDIRECTION_ASSERTION_JSON = new Sample("ob20/warning-with-redirection.json", true);
+ // original: test_validation: test_issuer_warn_on_non_https_id
+ public final static Sample WARNING_ISSUER_NON_HTTPS_JSON = new Sample("ob20/warning-issuer-non-http.json", true);
+ // original: test_validation: test_can_input_badgeclass
+ public final static Sample SIMPLE_BADGECLASS = new Sample("ob20/assets/badgeclass1.json", true);
+ // original: test_validation: test_validate_compacted_iri_value
+ public final static Sample ISSUER_COMPACTIRI_VALIDATION = new Sample("ob20/issuer-compact-iri-validation.json", true);
+ // original: validate_language: validate_language_prop_basic
+ public final static Sample SIMPLE_LANGUAGE_BADGECLASS = new Sample("ob20/badge-class-with-language.json", true);
+ // original: test_validation: test_validate_in_context_string_type
+ public final static Sample RDF_VALIDATION_VALID_BADGE_CLASS = new Sample("ob20/rdf-validation/valid-badge-class.json", true);
+ public final static Sample RDF_VALIDATION_VALID_ISSUER_EXTENSION_CLASS = new Sample("ob20/rdf-validation/valid-issuer-extension.json", true);
+ public final static Sample RDF_VALIDATION_VALID_ALIGNMENT_OBJECT = new Sample("ob20/rdf-validation/valid-alignment-object.json", true);
+ public final static Sample RDF_VALIDATION_VALID_EXTERNAL_CLASS = new Sample("ob20/rdf-validation/valid-cool-class.json", true);
+ public final static Sample RDF_VALIDATION_INVALID_CLASS = new Sample("ob20/rdf-validation/invalid-class.json", true);
+ public final static Sample RDF_VALIDATION_INVALID_EMPTY_CLASS = new Sample("ob20/rdf-validation/invalid-empty-type.json", true);
+ public final static Sample RDF_VALIDATION_INVALID_ELEM_CLASS = new Sample("ob20/rdf-validation/invalid-one-invalid-class.json", true);
+ public final static Sample RDF_VALIDATION_INVALID_ISSUER_TYPE = new Sample("ob20/rdf-validation/badge-class-invalid-issuer-type.json", true);
+ public final static Sample RDF_VALIDATION_VALID_EMPTY_CRITERIA_TYPE = new Sample("ob20/rdf-validation/valid-badge-class-empty-criteria-type.json", true);
+ // otiginal: test_validation: test_hosted_verification_object_in_assertion
+ public final static Sample ISSUER_WITH_ALLOWED_ORIGINS = new Sample("ob20/basic-assertion-with-allowed-origins.json", true);
+ public final static Sample ISSUER_WITH_ALLOWED_ORIGINS_VALID_STARTSWITH = new Sample("ob20/basic-assertion-with-allowed-origins-valid-starts-with.json", true);
+ public final static Sample ISSUER_WITH_ALLOWED_ORIGINS_INVALID_STARTSWITH = new Sample("ob20/basic-assertion-with-allowed-origins-invalid-starts-with.json", true);
+ public final static Sample ISSUER_WITH_ALLOWED_ORIGINS_VALID_MULTIPLE_STARTSWITH = new Sample("ob20/basic-assertion-with-allowed-origins-valid-multiple-starts-with.json", true);
+ public final static Sample ISSUER_WITH_ALLOWED_ORIGINS_INVALID_MULTIPLE_STARTSWITH = new Sample("ob20/basic-assertion-with-allowed-origins-invalid-multiple-starts-with.json", true);
+ // original: test_validation: test_assertion_not_expired
+ public final static Sample SIMPLE_EXPIRED_ASSERTION_JSON = new Sample("ob20/basic-assertion-expired.json", true);
+ // original: test_validation: test_assertion_not_expires_before_issue
+ public final static Sample SIMPLE_EXPIRED_BEFORE_ISSUED_ASSERTION_JSON = new Sample("ob20/basic-assertion-expired-before-issued.json", true);
+ // original: test_validation: test_assertion_not_issued_in_future
+ public final static Sample SIMPLE_FUTURE_ASSERTION_JSON = new Sample("ob20/basic-assertion-in-future.json", true);
+ // original: test_validate_related: test_validate_related_language
+ public final static Sample BASIC_WITH_LANGUAGE_JSON = new Sample("ob20/basic-assertion-with-language.json", true);
+ // original: test_image_validation: test_base64_data_uri_in_badgeclass
+ public final static Sample BADGE_WITH_DATA_IMAGE_JSON = new Sample("ob20/assets/badge-with-data-image.json", true);
+ // original: test_image_validation: test_base64_data_uri_in_assertion
+ public final static Sample ASSERTION_WITH_DATA_IMAGE_JSON = new Sample("ob20/assertion-with-data-image.json", true);
+ // original: test_image_validation: test_validate_badgeclass_image_formats
+ public final static Sample BADGE_WITH_COMPLEX_IMAGE_JSON = new Sample("ob20/assets/badgeclass-with-complex-image.json", true);
+ // original: test_validate_endorsements
+ public final static Sample ASSERTION_WITH_ENDORSEMENTS = new Sample("ob20/assertion-with-endorsements.json", true);
+ // original: test_validate_extensions: test_validate_extension_node_basic
+ public final static Sample ASSERTION_WITH_EXTENSION_NODE_BASIC_JSON = new Sample("ob20/assertion-with-extension-node-basic.json", true);
+ // original: test_validate_extensions: test_validate_extension_node_invalid
+ public final static Sample ASSERTION_WITH_EXTENSION_NODE_INVALID_JSON = new Sample("ob20/assertion-with-extension-node-invalid.json", true);
+ // original: test_validate_extensions: test_validation_breaks_down_multiple_extensions
+ public final static Sample ASSERTION_WITH_MULTIPLE_EXTENSIONS_JSON = new Sample("ob20/assertion-with-multiple-extensions.json", true);
+ }
+
+ public static final class PNG {
+ // original: test_verify: test_verify_of_baked_image
+ public final static Sample SIMPLE_JSON_PNG = new Sample("ob20/simple-badge.png", true);
+ }
+
+ public static final class JWT {
+ // original: test_signed_verification: test_can_full_verify_jws_signed_assertion
+ public final static Sample SIMPLE_JWT = new Sample("ob20/simple.jwt", true);
+ // original: test_signed_verification: test_can_full_verify_with_revocation_check
+ public final static Sample SIMPLE_NOT_REVOKED_JWT = new Sample("ob20/simple-not-revoked.jwt", true);
+ // original: test_signed_verification: test_revoked_badge_marked_invalid
+ public final static Sample SIMPLE_REVOKED_JWT = new Sample("ob20/simple-revoked.jwt", true);
+ }
+ }
}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java
index 28b8e7c..d0893e2 100644
--- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/credential/PayloadParserTests.java
@@ -1,10 +1,9 @@
package org.oneedtech.inspect.vc.credential;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
-import java.util.Optional;
-
import org.junit.jupiter.api.Test;
import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.RunContext.Key;
@@ -15,33 +14,22 @@ 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.VerifiableCredential;
import org.oneedtech.inspect.vc.payload.PayloadParser;
import org.oneedtech.inspect.vc.payload.PayloadParserFactory;
+import org.oneedtech.inspect.vc.payload.PngParser;
+import org.oneedtech.inspect.vc.payload.SvgParser;
import com.fasterxml.jackson.databind.ObjectMapper;
public class PayloadParserTests {
-
- @Test
+
+ @Test
void testSvgStringExtract() {
assertDoesNotThrow(()->{
Resource res = Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource(ResourceType.SVG);
PayloadParser ext = PayloadParserFactory.of(res);
- assertNotNull(ext);
- Credential crd = ext.parse(res, mockOB30Context(res));
- //System.out.println(crd.getJson().toPrettyString());
- assertNotNull(crd);
- assertNotNull(crd.getJson());
- assertNotNull(crd.getJson().get("@context"));
- });
- }
-
- @Test
- void testSvgJwtExtract() {
- assertDoesNotThrow(()->{
- Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG);
- PayloadParser ext = PayloadParserFactory.of(res);
- assertNotNull(ext);
+ assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
@@ -50,12 +38,26 @@ public class PayloadParserTests {
});
}
- @Test
+ @Test
+ void testSvgJwtExtract() {
+ assertDoesNotThrow(()->{
+ Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG);
+ PayloadParser ext = PayloadParserFactory.of(res);
+ assertNotNull(ext);
+ 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);
+ assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
@@ -63,13 +65,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context"));
});
}
-
- @Test
+
+ @Test
void testPngJwtExtract() {
assertDoesNotThrow(()->{
Resource res = Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource(ResourceType.PNG);
PayloadParser ext = PayloadParserFactory.of(res);
- assertNotNull(ext);
+ assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
@@ -77,13 +79,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context"));
});
}
-
- @Test
+
+ @Test
void testJwtExtract() {
assertDoesNotThrow(()->{
Resource res = Samples.OB30.JWT.SIMPLE_JWT.asFileResource(ResourceType.JWT);
PayloadParser ext = PayloadParserFactory.of(res);
- assertNotNull(ext);
+ assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
@@ -91,13 +93,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context"));
});
}
-
- @Test
+
+ @Test
void testJsonExtract() {
assertDoesNotThrow(()->{
Resource res = Samples.OB30.JSON.SIMPLE_JSON.asFileResource(ResourceType.JSON);
PayloadParser ext = PayloadParserFactory.of(res);
- assertNotNull(ext);
+ assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
@@ -105,7 +107,7 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context"));
});
}
-
+
private RunContext mockOB30Context(Resource res) {
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
@@ -114,6 +116,10 @@ public class PayloadParserTests {
.put(res)
.put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath)
+ .put(Key.GENERATED_OBJECT_BUILDER, new VerifiableCredential.Builder())
+ .put(Key.PNG_CREDENTIAL_KEY, PngParser.Keys.OB30)
+ .put(Key.SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB30)
+ .put(Key.JWT_CREDENTIAL_NODE_NAME, VerifiableCredential.JWT_NODE_NAME)
.build();
}
}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/resource/TestUriResourceFactory.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/resource/TestUriResourceFactory.java
new file mode 100644
index 0000000..b663bf5
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/resource/TestUriResourceFactory.java
@@ -0,0 +1,35 @@
+package org.oneedtech.inspect.vc.resource;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.oneedtech.inspect.util.resource.UriResource;
+import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
+
+import com.apicatalog.jsonld.loader.DocumentLoader;
+
+import foundation.identity.jsonld.ConfigurableDocumentLoader;
+
+/**
+ * UriResource factory for test, resolving local references
+ * @author xaracil
+ */
+public class TestUriResourceFactory implements UriResourceFactory {
+
+ final DocumentLoader documentLoader;
+
+ public TestUriResourceFactory(DocumentLoader documentLoader) {
+ this.documentLoader = documentLoader;
+ }
+
+ @Override
+ public UriResource of(String uriString) throws URISyntaxException {
+ URI uri = new URI(uriString);
+ if (documentLoader instanceof CachingDocumentLoader) {
+ URI resolvedUri = ((CachingDocumentLoader.HttpLoader) ConfigurableDocumentLoader.getDefaultHttpLoader()).resolve(uri);
+ uri = resolvedUri;
+ }
+ return new UriResource(uri);
+ }
+
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java
index b56542f..198e6ee 100644
--- a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/CachingDocumentLoaderTests.java
@@ -1,27 +1,47 @@
package org.oneedtech.inspect.vc.util;
import java.net.URI;
+import java.net.URL;
+import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.apicatalog.jsonld.document.Document;
+import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.loader.DocumentLoader;
import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
+import com.google.common.io.Resources;
public class CachingDocumentLoaderTests {
@Test
void testStaticCachedDocumentBundled() {
Assertions.assertDoesNotThrow(()->{
- DocumentLoader loader = new CachingDocumentLoader();
+ DocumentLoader loader = new CachingDocumentLoader();
for(String id : CachingDocumentLoader.bundled.keySet()) {
Document doc = loader.loadDocument(new URI(id), new DocumentLoaderOptions());
- Assertions.assertNotNull(doc);
- }
+ Assertions.assertNotNull(doc);
+ }
});
}
-
+
+ @Test
+ void testLocalDomainCachedDocument() {
+ Assertions.assertDoesNotThrow(()->{
+ Map localDomains = Map.of(new URI("http://example.org/"), "ob20");
+ DocumentLoader loader = new CachingDocumentLoader(localDomains);
+ URI uri = new URI("http://example.org/basic-assertion.json");
+ Document doc = loader.loadDocument(uri, new DocumentLoaderOptions());
+ Assertions.assertNotNull(doc);
+
+ // assert the returned document is the same as the local resource
+ URL resource = Resources.getResource("ob20/basic-assertion.json");
+ JsonDocument resourceDocument = JsonDocument.of(resource.openStream());
+ Assertions.assertEquals(resourceDocument.getJsonContent().toString(), doc.getJsonContent().toString());
+ });
+ }
+
@Test
void testStaticCachedDocumentKey() {
Assertions.assertDoesNotThrow(()->{
@@ -30,5 +50,5 @@ public class CachingDocumentLoaderTests {
Document doc = loader.loadDocument(uri, new DocumentLoaderOptions());
Assertions.assertNotNull(doc);
});
- }
+ }
}
\ No newline at end of file
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidatorTests.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidatorTests.java
new file mode 100644
index 0000000..8cd2052
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/PrimitiveValueValidatorTests.java
@@ -0,0 +1,162 @@
+package org.oneedtech.inspect.vc.util;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.oneedtech.inspect.util.json.ObjectMapperCache;
+import org.oneedtech.inspect.vc.Assertion.ValueType;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Test case for PrimitiveValueValidator.
+ * Maps to "PropertyValidationTests" in python implementation
+ */
+public class PrimitiveValueValidatorTests {
+ private static ObjectMapper mapper;
+
+ @BeforeAll
+ static void setup() {
+ mapper = ObjectMapperCache.get(DEFAULT);
+ }
+
+ @Test
+ void testDataUri() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs=",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",
+ "data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678",
+ "data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh",
+ "data:,actually%20a%20valid%20data%20URI",
+ "data:,");
+ List badValues = List.of("data:image/gif",
+ "http://someexample.org",
+ "data:bad:path");
+ assertFunction(ValueType.DATA_URI, goodValues, badValues);
+ }
+
+ @Test
+ void testDataUriOrUrl() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs=",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",
+ "data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678",
+ "data:text/vnd-example+xyz;foo=bar;base64,R0lGODdh",
+ "http://www.example.com:8080/", "http://www.example.com:8080/foo/bar",
+ "http://www.example.com/foo%20bar", "http://www.example.com/foo/bar?a=b&c=d",
+ "http://www.example.com/foO/BaR", "HTTPS://www.EXAMPLE.cOm/",
+ "http://142.42.1.1:8080/", "http://142.42.1.1/",
+ "http://foo.com/blah_(wikipedia)#cite-1", "http://a.b-c.de",
+ "http://userid:password@example.com/", "http://-.~:%40:80%2f:password@example.com",
+ "http://code.google.com/events/#&product=browser");
+ List badValues = List.of("///", "///f", "//",
+ "rdar://12345", "h://test", ":// should fail", "", "a",
+ "urn:uuid:129487129874982374", "urn:uuid:9d278beb-36cf-4bc8-888d-674ff9843d72");
+ assertFunction(ValueType.DATA_URI_OR_URL, goodValues, badValues);
+ }
+
+ @Test
+ void testUrl() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("http://www.example.com:8080/", "http://www.example.com:8080/foo/bar",
+ "http://www.example.com/foo%20bar", "http://www.example.com/foo/bar?a=b&c=d",
+ "http://www.example.com/foO/BaR", "HTTPS://www.EXAMPLE.cOm/",
+ "http://142.42.1.1:8080/", "http://142.42.1.1/", "http://localhost:3000/123",
+ "http://foo.com/blah_(wikipedia)#cite-1", "http://a.b-c.de",
+ "http://userid:password@example.com/", "http://-.~:%40:80%2f:password@example.com",
+ "http://code.google.com/events/#&product=browser");
+ List badValues = List.of("data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs=", "///", "///f", "//",
+ "rdar://12345", "h://test", ":// should fail", "", "a",
+ "urn:uuid:129487129874982374", "urn:uuid:9d278beb-36cf-4bc8-888d-674ff9843d72");
+ assertFunction(ValueType.URL, goodValues, badValues);
+ }
+
+ @Test
+ void testIri() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("http://www.example.com:8080/", "_:b0", "_:b12", "_:b107", "_:b100000001232",
+ "urn:uuid:9d278beb-36cf-4bc8-888d-674ff9843d72",
+ "urn:uuid:9D278beb-36cf-4bc8-888d-674ff9843d72");
+ List badValues = List.of("data:image/gif;base64,R0lGODlhyAAiALM...DfD0QAADs=", "urn:uuid", "urn:uuid:123",
+ "", "urn:uuid:", "urn:uuid:zz278beb-36cf-4bc8-888d-674ff9843d72");
+ assertFunction(ValueType.IRI, goodValues, badValues);
+ }
+
+ @Test
+ void testUrlAuthority() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("google.com", "nerds.example.com");
+ List badValues = List.of("666", "http://google.com/", "https://www.google.com/search?q=murder+she+wrote&oq=murder+she+wrote",
+ "ftp://123.123.123.123", "bears", "lots of hungry bears", "bears.com/thewoods",
+ "192.168.0.1", "1::6:7:8");
+ assertFunction(ValueType.URL_AUTHORITY, goodValues, badValues);
+ }
+
+ @Test
+ void testCompactedIRI() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("id", "email", "telephone", "url");
+ List badValues = List.of("sloths");
+ assertFunction(ValueType.COMPACT_IRI, goodValues, badValues);
+ }
+
+ @Test
+ void testBasicText() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("string value");
+ List badValues = List.of(3, 4);
+ assertFunction(ValueType.TEXT, goodValues, badValues);
+ }
+
+ @Test
+ void testTelephone() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("+64010", "+15417522845", "+18006664358", "+18006662344;ext=666");
+ List badValues = List.of("1-800-666-DEVIL", "1 (555) 555-5555", "+99 55 22 1234", "+18006664343 x666");
+ assertFunction(ValueType.TELEPHONE, goodValues, badValues);
+ }
+
+ @Test
+ void testEmail() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("abc@localhost", "cool+uncool@example.org");
+ List badValues = List.of(" spacey@gmail.com", "steveman [at] gee mail dot com");
+ assertFunction(ValueType.EMAIL, goodValues, badValues);
+ }
+
+ @Test
+ void testBoolean() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of(true, false);
+ List badValues = List.of(" spacey@gmail.com", "steveman [at] gee mail dot com");
+ assertFunction(ValueType.BOOLEAN, goodValues, badValues);
+ }
+
+ @Test
+ void testDateTime() throws JsonMappingException, JsonProcessingException {
+ List goodValues = List.of("1977-06-10T12:00:00+0800",
+ "1977-06-10T12:00:00-0800",
+ "1977-06-10T12:00:00+08",
+ "1977-06-10T12:00:00+08:00");
+ List badValues = List.of("notadatetime", "1977-06-10T12:00:00");
+ assertFunction(ValueType.DATETIME, goodValues, badValues);
+ }
+
+ private void assertFunction(ValueType valueType, List extends Object> goodValues, List extends Object> badValues) throws JsonMappingException, JsonProcessingException {
+ Function validationFunction = valueType.getValidationFunction();
+ for (Object goodValue : goodValues) {
+ assertTrue(validationFunction.apply(parseNode(goodValue)),
+ "`" + goodValue + "` should pass " + valueType + " validation but failed.");
+ }
+ for (Object badValue : badValues) {
+ assertFalse(validationFunction.apply(parseNode(badValue)),
+ "`" + badValue + "` should fail " + valueType + " validation but passed.");
+ }
+ }
+
+ private JsonNode parseNode(Object value) throws JsonMappingException, JsonProcessingException {
+ if (value instanceof String) {
+ return mapper.readTree("\"" + value + "\"");
+ }
+ return mapper.readTree(value.toString());
+ }
+}
diff --git a/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/TestOB20Inspector.java b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/TestOB20Inspector.java
new file mode 100644
index 0000000..c98b273
--- /dev/null
+++ b/inspector-vc/src/test/java/org/oneedtech/inspect/vc/util/TestOB20Inspector.java
@@ -0,0 +1,64 @@
+package org.oneedtech.inspect.vc.util;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.oneedtech.inspect.util.resource.ResourceType;
+import org.oneedtech.inspect.util.spec.Specification;
+import org.oneedtech.inspect.vc.OB20Inspector;
+import org.oneedtech.inspect.vc.resource.TestUriResourceFactory;
+import org.oneedtech.inspect.vc.resource.UriResourceFactory;
+
+import com.apicatalog.jsonld.loader.DocumentLoader;
+
+/**
+ * OpenBadges 2.0 Test inspector.
+ * It's a subclass of main OB2.0 inspector, setting redirection of urls to local resources for testing
+ */
+public class TestOB20Inspector extends OB20Inspector {
+ protected final Map localDomains;
+
+ protected TestOB20Inspector(TestBuilder builder) {
+ super(builder);
+ if (getBehavior(OB20Inspector.Behavior.ALLOW_LOCAL_REDIRECTION) == Boolean.TRUE) {
+ this.localDomains = builder.localDomains;
+ } else {
+ this.localDomains = Collections.emptyMap();
+ }
+ }
+
+ @Override
+ protected DocumentLoader getDocumentLoader() {
+ return new CachingDocumentLoader(localDomains);
+ }
+
+ @Override
+ protected UriResourceFactory getUriResourceFactory(DocumentLoader documentLoader) {
+ return new TestUriResourceFactory(documentLoader);
+ }
+
+ public static class TestBuilder extends OB20Inspector.Builder {
+ final Map localDomains;
+
+ public TestBuilder() {
+ super();
+ // don't allow local redirections by default
+ super.behaviors.put(OB20Inspector.Behavior.ALLOW_LOCAL_REDIRECTION, true);
+ this.localDomains = new HashMap<>();
+ }
+
+ public TestBuilder add(URI localDomain, String resourcePath) {
+ localDomains.put(localDomain, resourcePath);
+ return this;
+ }
+
+ @Override
+ public TestOB20Inspector build() {
+ set(Specification.OB20);
+ set(ResourceType.OPENBADGE);
+ return new TestOB20Inspector(this);
+ }
+ }
+}
diff --git a/inspector-vc/src/test/resources/ob20/assertion-with-data-image.json b/inspector-vc/src/test/resources/ob20/assertion-with-data-image.json
new file mode 100644
index 0000000..338ba57
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assertion-with-data-image.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUMyqsBwACeQFChxlltgAAAABJRU5ErkJggg==",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assertion-with-endorsements.json b/inspector-vc/src/test/resources/ob20/assertion-with-endorsements.json
new file mode 100644
index 0000000..3af3824
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assertion-with-endorsements.json
@@ -0,0 +1,28 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": {
+ "type": "BadgeClass",
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "https://example.org/badgecriteria.json",
+ "issuer": "https://example.org/organization.json",
+ "endorsement": ["https://example.org/endorsement-3.json", "https://example.org/endorsement-4.json"]
+ },
+ "verification": {
+ "type": "hosted"
+ },
+ "endorsement": ["https://example.org/endorsement-1.json", "https://example.org/endorsement-2.json"]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-basic.json b/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-basic.json
new file mode 100644
index 0000000..9861d08
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-basic.json
@@ -0,0 +1,32 @@
+{
+ "@context": [
+ "https://w3id.org/openbadges/v2",
+ "https://w3id.org/openbadges/extensions/exampleExtension/context.json"
+ ],
+ "id": "http://example.org/assertion",
+ "type": "Assertion",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "badge": "https://example.org/robotics-badge.json",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "verification": {
+ "type": "hosted"
+ },
+ "extensions:exampleExtension": {
+ "id": "_:b0",
+ "type": [
+ "Extension",
+ "obi:extensions/#ExampleExtension"
+ ],
+ "http://schema.org/text": "I'm a property, short and sweet"
+ },
+ "evidence": {
+ "id": "_:b1",
+ "narrative": "Rocked the free world"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-invalid.json b/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-invalid.json
new file mode 100644
index 0000000..76194b3
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assertion-with-extension-node-invalid.json
@@ -0,0 +1,32 @@
+{
+ "@context": [
+ "https://w3id.org/openbadges/v2",
+ "https://w3id.org/openbadges/extensions/exampleExtension/context.json"
+ ],
+ "id": "http://example.org/assertion",
+ "type": "Assertion",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "badge": "https://example.org/robotics-badge.json",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "verification": {
+ "type": "hosted"
+ },
+ "extensions:exampleExtension": {
+ "id": "_:b0",
+ "type": [
+ "Extension",
+ "obi:extensions/#ExampleExtension"
+ ],
+ "http://schema.org/text": 1337
+ },
+ "evidence": {
+ "id": "_:b1",
+ "narrative": "Rocked the free world"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assertion-with-multiple-extensions.json b/inspector-vc/src/test/resources/ob20/assertion-with-multiple-extensions.json
new file mode 100644
index 0000000..51a4b48
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assertion-with-multiple-extensions.json
@@ -0,0 +1,35 @@
+{
+ "@context": [
+ "https://w3id.org/openbadges/v2",
+ "https://w3id.org/openbadges/extensions/exampleExtension/context.json",
+ "https://w3id.org/openbadges/extensions/applyLinkExtension/context.json"
+ ],
+ "id": "http://example.org/assertion",
+ "type": "Assertion",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "badge": "https://example.org/robotics-badge.json",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "verification": {
+ "type": "hosted"
+ },
+ "extensions:exampleExtension": {
+ "id": "_:b0",
+ "type": [
+ "Extension",
+ "obi:extensions/#ExampleExtension",
+ "extensions:ApplyLink"
+ ],
+ "http://schema.org/text": "I'm a property, short and sweet",
+ "url": "http://www.1edtech.org"
+ },
+ "evidence": {
+ "id": "_:b1",
+ "narrative": "Rocked the free world"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/altbadgeurl.json b/inspector-vc/src/test/resources/ob20/assets/altbadgeurl.json
new file mode 100644
index 0000000..588b5a5
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/altbadgeurl.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/robotics-badge.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "https://example.org/organization.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/bad-issuer.json b/inspector-vc/src/test/resources/ob20/assets/bad-issuer.json
new file mode 100644
index 0000000..d035c87
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/bad-issuer.json
@@ -0,0 +1,206 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "urn:uuid:2d391246-6e0d-4dab-906c-b29770bd7aa6",
+ "type": "Issuer",
+ "url": "http://example.com",
+ "email": "email@example.org",
+ "name": "some Issuer"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-empty-revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-empty-revocation-list.json
new file mode 100644
index 0000000..401502b
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-empty-revocation-list.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/badge-from-organization-with-empty-revocation-list.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "https://example.org/organization-with-empty-revocation-list.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-revocation-list.json
new file mode 100644
index 0000000..68e64fd
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badge-from-organization-with-revocation-list.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/badge-from-organization-with-revocation-list.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "https://example.org/organization-with-revocation-list.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badge-with-bad-issuer.json b/inspector-vc/src/test/resources/ob20/assets/badge-with-bad-issuer.json
new file mode 100644
index 0000000..f99c558
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badge-with-bad-issuer.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "http://example.org/badge-with-bad-issuer.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "https://example.org/bad-issuer.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badge-with-data-image.json b/inspector-vc/src/test/resources/ob20/assets/badge-with-data-image.json
new file mode 100644
index 0000000..da4abf9
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badge-with-data-image.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/badge-from-organization-with-revocation-list.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUMyqsBwACeQFChxlltgAAAABJRU5ErkJggg==",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "https://example.org/organization-with-revocation-list.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-complex-image.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-complex-image.json
new file mode 100644
index 0000000..e06df7e
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-complex-image.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/badgeclass-with-complex-image.json",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer1.json",
+ "image": {
+ "id": "http://example.org/beths-robot-badge.png",
+ "author": "http://someoneelse.org/1",
+ "caption": "A hexagon with attitude"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-endorsements.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-endorsements.json
new file mode 100644
index 0000000..181827d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-endorsements.json
@@ -0,0 +1,11 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "https://example.org/badgecriteria.json",
+ "issuer": "https://example.org/organization.json",
+ "endorsement": ["https://example.org/endorsement-3.json", "https://example.org/endorsement-4.json"]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-language.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-language.json
new file mode 100644
index 0000000..0e0eee7
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-language.json
@@ -0,0 +1,15 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/badgeclass-with-language.json",
+ "@language": "es",
+ "name": "Insignia fantastica",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "https://example.org/badgecriteria.json",
+ "issuer": "https://example.org/organization.json",
+ "related": {
+ "id": "https://example.org/robotics-badge.json",
+ "@language": "en-US"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-multiple-starts-with.json
new file mode 100644
index 0000000..5d8cbfd
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-multiple-starts-with.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass-with-verification-invalid-multiple-starts-with",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-with-allowed-origins-invalid-multiple-starts-with.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-starts-with.json
new file mode 100644
index 0000000..8ab4a1d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-invalid-starts-with.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass-with-verification-invalid-starts-with",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-with-allowed-origins-invalid-starts-with.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-multiple-starts-with.json
new file mode 100644
index 0000000..09f8a75
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-multiple-starts-with.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass-with-verification-valid-multiple-starts-with",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-with-allowed-origins-valid-multiple-starts-with.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-starts-with.json
new file mode 100644
index 0000000..c2f3c54
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification-valid-starts-with.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass-with-verification-valid-starts-with",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-with-allowed-origins-valid-starts-with.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification.json
new file mode 100644
index 0000000..5d6a82d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass-with-verification.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass-with-verification",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-with-allowed-origins.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgeclass1.json b/inspector-vc/src/test/resources/ob20/assets/badgeclass1.json
new file mode 100644
index 0000000..117d4c5
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgeclass1.json
@@ -0,0 +1,208 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/badgeclass1",
+ "type": "BadgeClass",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer1.json",
+ "image": "http://example.org/robotics-badge.png"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/badgecriteria.json b/inspector-vc/src/test/resources/ob20/assets/badgecriteria.json
new file mode 100644
index 0000000..715c42f
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/badgecriteria.json
@@ -0,0 +1,3 @@
+{
+ "narrative": "Do the important things."
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/basic-badge-no-public-key.json b/inspector-vc/src/test/resources/ob20/assets/basic-badge-no-public-key.json
new file mode 100644
index 0000000..f47916b
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/basic-badge-no-public-key.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/basic-badge-no-public-key.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "https://example.org/badgecriteria.json",
+ "issuer": "https://example.org/organization-no-public-key.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/beths-robot-badge.png b/inspector-vc/src/test/resources/ob20/assets/beths-robot-badge.png
new file mode 100644
index 0000000..abdc4b6
Binary files /dev/null and b/inspector-vc/src/test/resources/ob20/assets/beths-robot-badge.png differ
diff --git a/inspector-vc/src/test/resources/ob20/assets/beths-robot-work.html b/inspector-vc/src/test/resources/ob20/assets/beths-robot-work.html
new file mode 100644
index 0000000..e69de29
diff --git a/inspector-vc/src/test/resources/ob20/assets/criteria-no-type.json b/inspector-vc/src/test/resources/ob20/assets/criteria-no-type.json
new file mode 100644
index 0000000..38b803a
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/criteria-no-type.json
@@ -0,0 +1,203 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "_:b0",
+ "narrative": "Do the important things."
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/empty-revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/empty-revocation-list.json
new file mode 100644
index 0000000..4a0e2d5
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/empty-revocation-list.json
@@ -0,0 +1,6 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/empty-revocation-list.json",
+ "type": "RevocationList",
+ "revokedAssertions": []
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/endorsement-1.json b/inspector-vc/src/test/resources/ob20/assets/endorsement-1.json
new file mode 100644
index 0000000..afa1695
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/endorsement-1.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/endorsement-1.json",
+ "type": "Endorsement",
+ "claim": {
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "endorsementComment": "Pretty good"
+ },
+ "issuedOn": "2017-10-01T00:00Z",
+ "issuer": "http://example.org/issuer1.json",
+ "verification": {
+ "type": "HostedBadge"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/endorsement-2.json b/inspector-vc/src/test/resources/ob20/assets/endorsement-2.json
new file mode 100644
index 0000000..5bbf4cf
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/endorsement-2.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/endorsement-2.json",
+ "type": "Endorsement",
+ "claim": {
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "endorsementComment": "Pretty good"
+ },
+ "issuedOn": "2017-10-01T00:00Z",
+ "issuer": "http://example.org/issuer1.json",
+ "verification": {
+ "type": "HostedBadge"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/endorsement-3.json b/inspector-vc/src/test/resources/ob20/assets/endorsement-3.json
new file mode 100644
index 0000000..89000e3
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/endorsement-3.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/endorsement-3.json",
+ "type": "Endorsement",
+ "claim": {
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "endorsementComment": "Pretty good"
+ },
+ "issuedOn": "2017-10-01T00:00Z",
+ "issuer": "http://example.org/issuer1.json",
+ "verification": {
+ "type": "HostedBadge"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/endorsement-4.json b/inspector-vc/src/test/resources/ob20/assets/endorsement-4.json
new file mode 100644
index 0000000..1100f8b
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/endorsement-4.json
@@ -0,0 +1,14 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/endorsement-4.json",
+ "type": "Endorsement",
+ "claim": {
+ "id": "https://example.org/badgeclass-with-endorsements.json",
+ "endorsementComment": "Pretty good"
+ },
+ "issuedOn": "2017-10-01T00:00Z",
+ "issuer": "http://example.org/issuer1.json",
+ "verification": {
+ "type": "HostedBadge"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-invalid-type.json b/inspector-vc/src/test/resources/ob20/assets/issuer-invalid-type.json
new file mode 100644
index 0000000..402e76a
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-invalid-type.json
@@ -0,0 +1,203 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-invalid-type",
+ "type": "Criteria"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-multiple-starts-with.json
new file mode 100644
index 0000000..1291f2c
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-multiple-starts-with.json
@@ -0,0 +1,209 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-with-allowed-origins",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org",
+ "verification": {
+ "startsWith": ["https://example.org/NOT", "http://example.com/ALSONOT"]
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-starts-with.json
new file mode 100644
index 0000000..fde8434
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-invalid-starts-with.json
@@ -0,0 +1,209 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-with-allowed-origins",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org",
+ "verification": {
+ "startsWith": "https://example.org/NOT"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-multiple-starts-with.json
new file mode 100644
index 0000000..f30a07d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-multiple-starts-with.json
@@ -0,0 +1,209 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-with-allowed-origins",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org",
+ "verification": {
+ "startsWith": ["https://example.org/", "http://example.com/assert"]
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-starts-with.json b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-starts-with.json
new file mode 100644
index 0000000..c2aa2df
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins-valid-starts-with.json
@@ -0,0 +1,209 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-with-allowed-origins",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org",
+ "verification": {
+ "startsWith": "https://example.org/"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins.json b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins.json
new file mode 100644
index 0000000..ae613e1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer-with-allowed-origins.json
@@ -0,0 +1,209 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer-with-allowed-origins",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org",
+ "verification": {
+ "allowedOrigins": ["example.com", "example.org"]
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/issuer1.json b/inspector-vc/src/test/resources/ob20/assets/issuer1.json
new file mode 100644
index 0000000..9b3d7a0
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/issuer1.json
@@ -0,0 +1,206 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.org/issuer1.json",
+ "type": "Issuer",
+ "name": "Example Issuer",
+ "email": "me@example.org",
+ "url": "http://example.org"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/key1.json b/inspector-vc/src/test/resources/ob20/assets/key1.json
new file mode 100644
index 0000000..4e9f1b0
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/key1.json
@@ -0,0 +1,7 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/key1.json",
+ "type": "CryptographicKey",
+ "owner": "https://example.org/organization.json",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAko9t/QZEwRUainRO5MqY\nGBBeFe7rm/BVg6ugkgkyRGdDkjCeEjNfCm6An+bQqeCsHoMCprs629/a9yJ58/4w\nP7ws/5lJ9YwZYXpGvLfha0xrpWl5DiExeOMB9pB8p4ze0AI6H49w9ZX0G+cgaqaK\n/FbQ/Ln1zkZH6+VaAntoaLu3I5kLLR1F31HqLmSKpm6rHyLFkq534dGfuNmjSM+x\nZxBKu+ugmyeL9DinHvBZ/dwFMGS+o4/bT54UeLMd7a71ONevRY2hXgwarfWjqkDs\nAgqnfKSDjjDZQDM2OlyLWvyI2SWF4eWxIUtWuFP4j0d/5SUFjhAldRLjIXMQWMEN\nNwIDAQAB\n-----END PUBLIC KEY-----"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/key2.json b/inspector-vc/src/test/resources/ob20/assets/key2.json
new file mode 100644
index 0000000..3fd6732
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/key2.json
@@ -0,0 +1,7 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/key2.json",
+ "type": "CryptographicKey",
+ "owner": "https://example.org/organization-with-empty-revocation-list.json",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjjtSHqVnCOhOe91mt1hh\nWa0h6qF5K32ETY3+6ENQ7kR6qvI1S4prxptYfkuCncJFeZkCu306QS8E/iYCUEoC\ndKOYMVtn1Tm16p2cD+d4LQNYDQbKyKIqU8Cgqb3GoS2MWlJ6Q9sVW1B24RYAokW9\npm80xFJMJAIbZKBfr/hMLofGD5vvBHV/UFYtGvy7PByA7eNPkSLFnIY2L6WBWVyE\na2o95Jzvih+H2CmAk6HSumJL/PdV3yys/m4TEmU0V3m7B8voa3yEIFYG4hKxGUCi\nRqaq/wTFsTVBsEZn9rcpjG58ibjfd1+HBhWjRqsa5NNM9sQqnvvNBSWnqE5ggiNc\nzQIDAQAB\n-----END PUBLIC KEY-----"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/key3.json b/inspector-vc/src/test/resources/ob20/assets/key3.json
new file mode 100644
index 0000000..f66e27c
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/key3.json
@@ -0,0 +1,7 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/key3.json",
+ "type": "CryptographicKey",
+ "owner": "https://example.org/organization-with-revocation-list.json",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlxjYVv/2hAXO/D9b/Ifq\ng4n8TQCG3tq6JUyQPrhW27E9htH5UexdbOAAlzydQ5kYve3F8fhHuue/mjnKrMJn\najdTU4BBIPT4CJ3ZCCzKZf9OvMwobZT5P5QmkG1eVLOTGFs9BTvpbGQ7qugUbxHH\nXeHK/OoBnenKmLV0R4+KePe0oTcNToDQbx0uMZzNM9TRpnVBHlCXwYbFzw1js0Tt\nuT4M8MtKOoyNf+ZA8fcsSpmE7K1xDYOY7o9tZMFtGIQuhKlZyVpKpAu4KrhrZBJG\nJ47zcxzrncQHmft3aL8qWNCtZ+fOa+/fPkjT+bPhCwHSkE9Xj8Q0AF1vmDNReun1\nhwIDAQAB\n-----END PUBLIC KEY-----"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/openbadges_context.json b/inspector-vc/src/test/resources/ob20/assets/openbadges_context.json
new file mode 100644
index 0000000..7c1caf1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/openbadges_context.json
@@ -0,0 +1,203 @@
+https://w3id.org/openbadges/v2
+
+{
+ "@context": {
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "AlignmentObject": "schema:AlignmentObject",
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "sec": "https://w3id.org/security#",
+ "Criteria": "obi:Criteria",
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "id": "@id",
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "Profile": "obi:Profile",
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "FrameValidation": "obi:FrameValidation",
+ "validationFrame": "obi:validationFrame",
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "validationSchema": "obi:validationSchema",
+ "validatesType": "obi:validatesType",
+ "version": {
+ "@id": "schema:version"
+ },
+ "BadgeClass": "obi:BadgeClass",
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "RevocationList": "obi:RevocationList",
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "type": "@type",
+ "email": {
+ "@id": "schema:email"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "schema": "http://schema.org/",
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "Extension": "obi:Extension",
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "CryptographicKey": "sec:Key",
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "hosted": "obi:HostedBadge",
+ "dc": "http://purl.org/dc/terms/",
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "HostedBadge": "obi:HostedBadge",
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "verify": "verification",
+ "VerificationObject": "obi:VerificationObject",
+ "name": {
+ "@id": "schema:name"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "obi": "https://w3id.org/openbadges#",
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "cred": "https://w3id.org/credentials#",
+ "Image": "obi:Image",
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "IdentityObject": "obi:IdentityObject",
+ "signed": "obi:SignedBadge",
+ "Evidence": "obi:Evidence",
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "TypeValidation": "obi:TypeValidation",
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "SignedBadge": "obi:SignedBadge",
+ "validation": "obi:validation",
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "Issuer": "obi:Issuer"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/organization-no-public-key.json b/inspector-vc/src/test/resources/ob20/assets/organization-no-public-key.json
new file mode 100644
index 0000000..9687d0b
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/organization-no-public-key.json
@@ -0,0 +1,8 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Issuer",
+ "id": "https://example.org/organization-no-public-key.json",
+ "name": "An Example Badge Issuer",
+ "url": "https://example.org",
+ "email": "contact@example.org"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/organization-with-empty-revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/organization-with-empty-revocation-list.json
new file mode 100644
index 0000000..1f6ce56
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/organization-with-empty-revocation-list.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Issuer",
+ "id": "https://example.org/organization-with-empty-revocation-list.json",
+ "name": "An Example Badge Issuer",
+ "url": "https://example.org",
+ "email": "contact@example.org",
+ "revocationList": "http://example.org/empty-revocation-list.json",
+ "publicKey": "http://example.org/key2.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/organization-with-revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/organization-with-revocation-list.json
new file mode 100644
index 0000000..6e4d47b
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/organization-with-revocation-list.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Issuer",
+ "id": "https://example.org/organization-with-revocation-list.json",
+ "name": "An Example Badge Issuer",
+ "url": "https://example.org",
+ "email": "contact@example.org",
+ "revocationList": "http://example.org/revocation-list.json",
+ "publicKey": "http://example.org/key3.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/organization.json b/inspector-vc/src/test/resources/ob20/assets/organization.json
new file mode 100644
index 0000000..1ae00aa
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/organization.json
@@ -0,0 +1,9 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Issuer",
+ "id": "https://example.org/organization.json",
+ "name": "An Example Badge Issuer",
+ "url": "https://example.org",
+ "email": "contact@example.org",
+ "publicKey": "http://example.org/key1.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/revocation-list.json b/inspector-vc/src/test/resources/ob20/assets/revocation-list.json
new file mode 100644
index 0000000..809f645
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/revocation-list.json
@@ -0,0 +1,15 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/revocation-list.json",
+ "type": "RevocationList",
+ "revokedAssertions": [
+ {
+ "id": "https://example.org/beths-robotics-badge-revoked.json",
+ "revocationReason": "A good reason, for sure"
+ },
+ {
+ "id": "urn:uuid:52e4c6b3-8c13-4fa8-8482-a5cf34ef37a9"
+ },
+ "urn:uuid:6deb4a00-ebce-4b28-8cc2-afa705ef7be4"
+ ]
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/robotics-badge.json b/inspector-vc/src/test/resources/ob20/assets/robotics-badge.json
new file mode 100644
index 0000000..118fae0
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/assets/robotics-badge.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "BadgeClass",
+ "id": "https://example.org/robotics-badge.json",
+ "name": "Awesome Robotics Badge",
+ "description": "For doing awesome things with robots that people think is pretty great.",
+ "image": "https://example.org/robotics-badge.png",
+ "criteria": "https://example.org/badgecriteria.json",
+ "issuer": "https://example.org/organization.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/assets/robotics-badge.png b/inspector-vc/src/test/resources/ob20/assets/robotics-badge.png
new file mode 100644
index 0000000..abdc4b6
Binary files /dev/null and b/inspector-vc/src/test/resources/ob20/assets/robotics-badge.png differ
diff --git a/inspector-vc/src/test/resources/ob20/badge-class-with-language.json b/inspector-vc/src/test/resources/ob20/badge-class-with-language.json
new file mode 100644
index 0000000..36b8e2e
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/badge-class-with-language.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/badgeclass",
+ "@language": "en-US",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer1.json",
+ "type": "BadgeClass"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-expired-before-issued.json b/inspector-vc/src/test/resources/ob20/basic-assertion-expired-before-issued.json
new file mode 100644
index 0000000..8756ff7
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-expired-before-issued.json
@@ -0,0 +1,19 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2022-12-31T23:59:59Z",
+ "expires": "2022-11-15T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-expired.json b/inspector-vc/src/test/resources/ob20/basic-assertion-expired.json
new file mode 100644
index 0000000..ac21119
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-expired.json
@@ -0,0 +1,19 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "expires": "2022-11-15T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-in-future.json b/inspector-vc/src/test/resources/ob20/basic-assertion-in-future.json
new file mode 100644
index 0000000..d0234d1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-in-future.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2100-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-context.json b/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-context.json
new file mode 100644
index 0000000..b1de0b9
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-context.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/invalid",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-type.json b/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-type.json
new file mode 100644
index 0000000..527cd67
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-invalid-type.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "OtherAssertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-no-public-key.json b/inspector-vc/src/test/resources/ob20/basic-assertion-no-public-key.json
new file mode 100644
index 0000000..e68a678
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-no-public-key.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/basic-badge-no-public-key.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-multiple-starts-with.json
new file mode 100644
index 0000000..93817d1
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-multiple-starts-with.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-verification-invalid-multiple-starts-with.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-starts-with.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-starts-with.json
new file mode 100644
index 0000000..51a8de0
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-invalid-starts-with.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-verification-invalid-starts-with.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-multiple-starts-with.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-multiple-starts-with.json
new file mode 100644
index 0000000..0e1b774
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-multiple-starts-with.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-verification-valid-multiple-starts-with.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-starts-with.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-starts-with.json
new file mode 100644
index 0000000..74f1b1e
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins-valid-starts-with.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-verification-valid-starts-with.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins.json
new file mode 100644
index 0000000..e20b351
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-allowed-origins.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-verification.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion-with-language.json b/inspector-vc/src/test/resources/ob20/basic-assertion-with-language.json
new file mode 100644
index 0000000..daf3d18
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion-with-language.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "id": "https://example.org/beths-robotics-badge.json",
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/badgeclass-with-language.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/basic-assertion.json b/inspector-vc/src/test/resources/ob20/basic-assertion.json
new file mode 100644
index 0000000..69963d6
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/basic-assertion.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/issuer-compact-iri-validation.json b/inspector-vc/src/test/resources/ob20/issuer-compact-iri-validation.json
new file mode 100644
index 0000000..23f62cd
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/issuer-compact-iri-validation.json
@@ -0,0 +1,11 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Issuer",
+ "id": "https://example.org/organization.json",
+ "url": "https://example.org",
+ "name": "An Example Badge Issuer",
+ "email": "contact@example.org",
+ "verification": {
+ "verificationProperty": "id"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/badge-class-invalid-issuer-type.json b/inspector-vc/src/test/resources/ob20/rdf-validation/badge-class-invalid-issuer-type.json
new file mode 100644
index 0000000..04031de
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/badge-class-invalid-issuer-type.json
@@ -0,0 +1,10 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "id": "http://example.org/badgeclass",
+ "@language": "en-US",
+ "name": "Example Badge",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer-invalid-type.json",
+ "type": "BadgeClass"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-class.json b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-class.json
new file mode 100644
index 0000000..3efddad
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-class.json
@@ -0,0 +1,204 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": "NotAKnownClass",
+ "name": "Chumley"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-empty-type.json b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-empty-type.json
new file mode 100644
index 0000000..d381011
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-empty-type.json
@@ -0,0 +1,204 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": [],
+ "name": "Chumley"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-one-invalid-class.json b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-one-invalid-class.json
new file mode 100644
index 0000000..2109f08
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/invalid-one-invalid-class.json
@@ -0,0 +1,204 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": ["Issuer", "UNKNOWN"],
+ "name": "Chumley"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/valid-alignment-object.json b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-alignment-object.json
new file mode 100644
index 0000000..9739292
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-alignment-object.json
@@ -0,0 +1,206 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": "AlignmentObject",
+ "name": "Chumley",
+ "targetName": "target name",
+ "targetUrl": "http://example.com"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class-empty-criteria-type.json b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class-empty-criteria-type.json
new file mode 100644
index 0000000..03b6547
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class-empty-criteria-type.json
@@ -0,0 +1,207 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": "BadgeClass",
+ "name": "Chumley",
+ "description": "An example",
+ "criteria": "http://example.com/criteria-no-type.json",
+ "issuer": "http://example.org/issuer1.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class.json b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class.json
new file mode 100644
index 0000000..4a0cee7
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-badge-class.json
@@ -0,0 +1,207 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": "BadgeClass",
+ "name": "Chumley",
+ "description": "An example",
+ "criteria": "http://example.com/badgecriteria.json",
+ "issuer": "http://example.org/issuer1.json"
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/valid-cool-class.json b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-cool-class.json
new file mode 100644
index 0000000..bf15347
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-cool-class.json
@@ -0,0 +1,204 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": "http://example.com/CoolClass",
+ "name": "Chumley"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/rdf-validation/valid-issuer-extension.json b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-issuer-extension.json
new file mode 100644
index 0000000..ebc8f1d
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/rdf-validation/valid-issuer-extension.json
@@ -0,0 +1,206 @@
+{
+ "@context": {
+ "id": "@id",
+ "type": "@type",
+ "extensions": "https://w3id.org/openbadges/extensions#",
+ "obi": "https://w3id.org/openbadges#",
+ "validation": "obi:validation",
+ "cred": "https://w3id.org/credentials#",
+ "dc": "http://purl.org/dc/terms/",
+ "schema": "http://schema.org/",
+ "sec": "https://w3id.org/security#",
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
+ "AlignmentObject": "schema:AlignmentObject",
+ "CryptographicKey": "sec:Key",
+ "Endorsement": "cred:Credential",
+ "Assertion": "obi:Assertion",
+ "BadgeClass": "obi:BadgeClass",
+ "Criteria": "obi:Criteria",
+ "Evidence": "obi:Evidence",
+ "Extension": "obi:Extension",
+ "FrameValidation": "obi:FrameValidation",
+ "IdentityObject": "obi:IdentityObject",
+ "Image": "obi:Image",
+ "HostedBadge": "obi:HostedBadge",
+ "hosted": "obi:HostedBadge",
+ "Issuer": "obi:Issuer",
+ "Profile": "obi:Profile",
+ "RevocationList": "obi:RevocationList",
+ "SignedBadge": "obi:SignedBadge",
+ "signed": "obi:SignedBadge",
+ "TypeValidation": "obi:TypeValidation",
+ "VerificationObject": "obi:VerificationObject",
+ "author": {
+ "@id": "schema:author",
+ "@type": "@id"
+ },
+ "caption": {
+ "@id": "schema:caption"
+ },
+ "claim": {
+ "@id": "cred:claim",
+ "@type": "@id"
+ },
+ "created": {
+ "@id": "dc:created",
+ "@type": "xsd:dateTime"
+ },
+ "creator": {
+ "@id": "dc:creator",
+ "@type": "@id"
+ },
+ "description": {
+ "@id": "schema:description"
+ },
+ "email": {
+ "@id": "schema:email"
+ },
+ "endorsement": {
+ "@id": "cred:credential",
+ "@type": "@id"
+ },
+ "expires": {
+ "@id": "sec:expiration",
+ "@type": "xsd:dateTime"
+ },
+ "genre": {
+ "@id": "schema:genre"
+ },
+ "image": {
+ "@id": "schema:image",
+ "@type": "@id"
+ },
+ "name": {
+ "@id": "schema:name"
+ },
+ "owner": {
+ "@id": "sec:owner",
+ "@type": "@id"
+ },
+ "publicKey": {
+ "@id": "sec:publicKey",
+ "@type": "@id"
+ },
+ "publicKeyPem": {
+ "@id": "sec:publicKeyPem"
+ },
+ "related": {
+ "@id": "dc:relation",
+ "@type": "@id"
+ },
+ "startsWith": {
+ "@id": "http://purl.org/dqm-vocabulary/v1/dqm#startsWith"
+ },
+ "tags": {
+ "@id": "schema:keywords"
+ },
+ "targetDescription": {
+ "@id": "schema:targetDescription"
+ },
+ "targetFramework": {
+ "@id": "schema:targetFramework"
+ },
+ "targetName": {
+ "@id": "schema:targetName"
+ },
+ "targetUrl": {
+ "@id": "schema:targetUrl"
+ },
+ "telephone": {
+ "@id": "schema:telephone"
+ },
+ "url": {
+ "@id": "schema:url",
+ "@type": "@id"
+ },
+ "version": {
+ "@id": "schema:version"
+ },
+ "alignment": {
+ "@id": "obi:alignment",
+ "@type": "@id"
+ },
+ "allowedOrigins": {
+ "@id": "obi:allowedOrigins"
+ },
+ "audience": {
+ "@id": "obi:audience"
+ },
+ "badge": {
+ "@id": "obi:badge",
+ "@type": "@id"
+ },
+ "criteria": {
+ "@id": "obi:criteria",
+ "@type": "@id"
+ },
+ "endorsementComment": {
+ "@id": "obi:endorsementComment"
+ },
+ "evidence": {
+ "@id": "obi:evidence",
+ "@type": "@id"
+ },
+ "hashed": {
+ "@id": "obi:hashed",
+ "@type": "xsd:boolean"
+ },
+ "identity": {
+ "@id": "obi:identityHash"
+ },
+ "issuedOn": {
+ "@id": "obi:issueDate",
+ "@type": "xsd:dateTime"
+ },
+ "issuer": {
+ "@id": "obi:issuer",
+ "@type": "@id"
+ },
+ "narrative": {
+ "@id": "obi:narrative"
+ },
+ "recipient": {
+ "@id": "obi:recipient",
+ "@type": "@id"
+ },
+ "revocationList": {
+ "@id": "obi:revocationList",
+ "@type": "@id"
+ },
+ "revocationReason": {
+ "@id": "obi:revocationReason"
+ },
+ "revoked": {
+ "@id": "obi:revoked",
+ "@type": "xsd:boolean"
+ },
+ "revokedAssertions": {
+ "@id": "obi:revoked"
+ },
+ "salt": {
+ "@id": "obi:salt"
+ },
+ "targetCode": {
+ "@id": "obi:targetCode"
+ },
+ "uid": {
+ "@id": "obi:uid"
+ },
+ "validatesType": "obi:validatesType",
+ "validationFrame": "obi:validationFrame",
+ "validationSchema": "obi:validationSchema",
+ "verification": {
+ "@id": "obi:verify",
+ "@type": "@id"
+ },
+ "verificationProperty": {
+ "@id": "obi:verificationProperty"
+ },
+ "verify": "verification"
+ },
+ "id": "http://example.com/badge1",
+ "type": ["Issuer", "http://example.com/CoolClass"],
+ "name": "Chumley",
+ "url": "https://example.org",
+ "email": "contact@example.org"
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/redirected-validation-subject.json b/inspector-vc/src/test/resources/ob20/redirected-validation-subject.json
new file mode 100644
index 0000000..924a704
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/redirected-validation-subject.json
@@ -0,0 +1,18 @@
+{ // both http://example.org/altbadgeurl and https://example.org/beths-robotics-badge.json return this json
+ "@context": "https://w3id.org/openbadges/v2", // openbadges_context.json
+ "type": "Assertion",
+ "id": "http://example.org/altbadgeurl",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png", // image
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "https://example.org/robotics-badge.json",
+ "verification": {
+ "type": "hosted"
+ }
+ }
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/simple-badge.png b/inspector-vc/src/test/resources/ob20/simple-badge.png
new file mode 100644
index 0000000..b1c7493
Binary files /dev/null and b/inspector-vc/src/test/resources/ob20/simple-badge.png differ
diff --git a/inspector-vc/src/test/resources/ob20/simple-not-revoked.jwt b/inspector-vc/src/test/resources/ob20/simple-not-revoked.jwt
new file mode 100644
index 0000000..b60fb91
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/simple-not-revoked.jwt
@@ -0,0 +1 @@
+eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UuanNvbiIsInJlY2lwaWVudCI6eyJ0eXBlIjoiZW1haWwiLCJoYXNoZWQiOnRydWUsInNhbHQiOiJkZWFkc2VhIiwiaWRlbnRpdHkiOiJzaGEyNTYkZWNmNTQwOWYzZjRiOTFhYjYwY2M1ZWY0YzAyYWVmNzAzMjM1NDM3NWU3MGNmNGQ4ZTQzZjZhMWQyOTg5MTk0MiJ9LCJpbWFnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3QtYmFkZ2UucG5nIiwiZXZpZGVuY2UiOiJodHRwczovL2V4YW1wbGUub3JnL2JldGhzLXJvYm90LXdvcmsuaHRtbCIsImlzc3VlZE9uIjoiMjAxNi0xMi0zMVQyMzo1OTo1OVoiLCJiYWRnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmFkZ2UtZnJvbS1vcmdhbml6YXRpb24td2l0aC1lbXB0eS1yZXZvY2F0aW9uLWxpc3QuanNvbiIsInZlcmlmaWNhdGlvbiI6eyJ0eXBlIjoic2lnbmVkIiwiY3JlYXRvciI6Imh0dHA6Ly9leGFtcGxlLm9yZy9rZXkyLmpzb24ifX0.gLjqNnlBSJ3oWt84iPhVaFjYGR2bhrSLDnTHh5mcjujO_VxYFop5EJlGHV4C7UC6xnM01Yy4y8JxqCIvLLudVRrPtK3pYumWJvGnc43cNyjZq8IY6L1qrsklP_gJyV0P9IKUzh6kQDdSGx_1TwYY3qwKlLXqNuxqkjdcD0z3zlhYfCnEpk7GZAXP_1Np123g-R9i3OZ18cxcUrCo1W7fZGkH4NXayXvdQpGwKmyCiju0N3ZLbHtfcbdWyVLLDF1RJJuRoVRgETly4uqVu8aFJ9O62HuOJggF7eLTos1gg-M2IpCa7yuUDVqGCWIRun5tbuUbJv_0Kg68lCslpZ09aw
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/simple-revoked.jwt b/inspector-vc/src/test/resources/ob20/simple-revoked.jwt
new file mode 100644
index 0000000..b87743a
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/simple-revoked.jwt
@@ -0,0 +1 @@
+eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UtcmV2b2tlZC5qc29uIiwicmVjaXBpZW50Ijp7InR5cGUiOiJlbWFpbCIsImhhc2hlZCI6dHJ1ZSwic2FsdCI6ImRlYWRzZWEiLCJpZGVudGl0eSI6InNoYTI1NiRlY2Y1NDA5ZjNmNGI5MWFiNjBjYzVlZjRjMDJhZWY3MDMyMzU0Mzc1ZTcwY2Y0ZDhlNDNmNmExZDI5ODkxOTQyIn0sImltYWdlIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9iZXRocy1yb2JvdC1iYWRnZS5wbmciLCJldmlkZW5jZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3Qtd29yay5odG1sIiwiaXNzdWVkT24iOiIyMDE2LTEyLTMxVDIzOjU5OjU5WiIsImJhZGdlIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9iYWRnZS1mcm9tLW9yZ2FuaXphdGlvbi13aXRoLXJldm9jYXRpb24tbGlzdC5qc29uIiwidmVyaWZpY2F0aW9uIjp7InR5cGUiOiJzaWduZWQiLCJjcmVhdG9yIjoiaHR0cDovL2V4YW1wbGUub3JnL2tleTMuanNvbiJ9fQ.RPJ0gudgtSYwjlZLnMmPHuL_SZmlW46cD3GXE8JQgOk02RwofMUt1w14ey8x7nWexXhvoBIA63qT4S0MfVuMeznAzNMO9_Upm0fworkR500257402NoXTnyzvF8Z8j_bCfoDZrdvljHFCieE1txmhDvRLvR5_y8KaZ6XC3fggokqMUQj6Is6LO4rnazqQixZWT8h_mPbE18_lUi0ocvHdrkwVWIeQi8_B-vgsUOANoBkX9ALBwHnqgfmfOwd-zGV39rBY4lj2nAxoHmY4VClqRj3Qqzoml7bd2eIhFtkJUmxfFuKdcfVhknWqw72MDGb6T-iCKYjuBNhYdxw6i1yQg
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/simple.jwt b/inspector-vc/src/test/resources/ob20/simple.jwt
new file mode 100644
index 0000000..c4f1558
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/simple.jwt
@@ -0,0 +1 @@
+eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UuanNvbiIsInJlY2lwaWVudCI6eyJ0eXBlIjoiZW1haWwiLCJoYXNoZWQiOnRydWUsInNhbHQiOiJkZWFkc2VhIiwiaWRlbnRpdHkiOiJzaGEyNTYkZWNmNTQwOWYzZjRiOTFhYjYwY2M1ZWY0YzAyYWVmNzAzMjM1NDM3NWU3MGNmNGQ4ZTQzZjZhMWQyOTg5MTk0MiJ9LCJpbWFnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3QtYmFkZ2UucG5nIiwiZXZpZGVuY2UiOiJodHRwczovL2V4YW1wbGUub3JnL2JldGhzLXJvYm90LXdvcmsuaHRtbCIsImlzc3VlZE9uIjoiMjAxNi0xMi0zMVQyMzo1OTo1OVoiLCJiYWRnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvcm9ib3RpY3MtYmFkZ2UuanNvbiIsInZlcmlmaWNhdGlvbiI6eyJ0eXBlIjoic2lnbmVkIiwiY3JlYXRvciI6Imh0dHA6Ly9leGFtcGxlLm9yZy9rZXkxLmpzb24ifX0.bR5tt-GEneb7tdWfmDdCq2FCBRuvSnKxNIfMPVz4lXiLcC_MobAB1zC9VQTJ5wJMzyS7Z7SR6eqt5WjuV4gl-6cT1qm3Af4VaKMEpi5bc3w2EGMlw10YxYAIrFEmdAXod9jwA2hPd9k4ypuIIIyzThRAhxVY36vf6nZWkC11MNj0QJoAJ-q2JlaERaGnwSOf1h63jUIyWtsfs6ocXb_hiFIaxPKAqnJSnWeF672vo3lOG_0vM6Tk4Ug73DRzJOsJhGwSWWze_GmwgqtUcWS2b8HmpoEaS_1gXwqnhNxiNNN0a39P-MqP86cY6pb64WKcIVY0ZSnvb4mwzXzZ3cPOTg
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/warning-issuer-non-http.json b/inspector-vc/src/test/resources/ob20/warning-issuer-non-http.json
new file mode 100644
index 0000000..b48a321
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/warning-issuer-non-http.json
@@ -0,0 +1,17 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/warning-issuer-non-http.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "http://example.org/badge-with-bad-issuer.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file
diff --git a/inspector-vc/src/test/resources/ob20/warning-with-redirection.json b/inspector-vc/src/test/resources/ob20/warning-with-redirection.json
new file mode 100644
index 0000000..fcf0ccb
--- /dev/null
+++ b/inspector-vc/src/test/resources/ob20/warning-with-redirection.json
@@ -0,0 +1,18 @@
+{
+ "@context": "https://w3id.org/openbadges/v2",
+ "type": "Assertion",
+ "id": "https://example.org/beths-robotics-badge.json",
+ "recipient": {
+ "type": "email",
+ "hashed": true,
+ "salt": "deadsea",
+ "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
+ },
+ "image": "https://example.org/beths-robot-badge.png",
+ "evidence": "https://example.org/beths-robot-work.html",
+ "issuedOn": "2016-12-31T23:59:59Z",
+ "badge": "http://example.org/altbadgeurl.json",
+ "verification": {
+ "type": "hosted"
+ }
+}
\ No newline at end of file