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 goodValues, List 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