Merge pull request #59 from imsglc/dec22-release

Dec22 release
This commit is contained in:
Xavi Aracil 2022-12-19 11:15:17 +01:00 committed by GitHub
commit 79e3adb7a8
135 changed files with 10110 additions and 507 deletions

View File

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>org.1edtech</groupId> <groupId>org.1edtech</groupId>
<artifactId>inspector</artifactId> <artifactId>inspector</artifactId>
<version>0.9.8</version> <version>0.9.9</version>
</parent> </parent>
<artifactId>inspector-vc</artifactId> <artifactId>inspector-vc</artifactId>
<dependencies> <dependencies>

View File

@ -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<CredentialEnum, SchemaKey> 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<Validation> getValidations() {
return validationMap.get(assertionType);
}
private static final Map<CredentialEnum, SchemaKey> schemas = new ImmutableMap.Builder<CredentialEnum, SchemaKey>()
.put(Type.Assertion, Catalog.OB_21_ASSERTION_JSON)
.build();
public static class Builder extends Credential.Builder<Assertion> {
@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<Type> primaryObjects = List.of(Assertion, BadgeClass, Issuer, Profile, Endorsement);
private final List<String> allowedTypeValues;
private final boolean allowedTypeValuesRequired;
Type(List<String> typeValues) {
this(typeValues, true);
}
Type(List<String> typeValues, boolean allowedTypeValuesRequired) {
this.allowedTypeValues = typeValues;
this.allowedTypeValuesRequired = allowedTypeValuesRequired;
}
public static Assertion.Type valueOf (JsonNode typeNode) {
if(typeNode != null) {
List<String> 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<String> getRequiredTypeValues() {
return Collections.emptyList();
}
@Override
public List<String> getAllowedTypeValues() {
return allowedTypeValues;
}
@Override
public List<String> getContextUris() {
return List.of("https://w3id.org/openbadges/v2") ;
}
@Override
public boolean isAllowedTypeValuesRequired() {
return allowedTypeValuesRequired;
}
public List<Validation> 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<JsonNode, Boolean> validationFunction;
private ValueType(Function<JsonNode, Boolean> validationFunction) {
this.validationFunction = validationFunction;
}
public Function<JsonNode, Boolean> getValidationFunction() {
return validationFunction;
}
public static List<ValueType> 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<Type, List<Validation>> validationMap = new ImmutableMap.Builder<Type, List<Validation>>()
.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
}

View File

@ -1,133 +1,133 @@
package org.oneedtech.inspect.vc; package org.oneedtech.inspect.vc;
import static org.oneedtech.inspect.util.code.Defensives.*; import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import static org.oneedtech.inspect.util.resource.ResourceType.*; import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
import static org.oneedtech.inspect.vc.Credential.Type.*; 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.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.oneedtech.inspect.core.probe.GeneratedObject; import org.oneedtech.inspect.core.probe.GeneratedObject;
import org.oneedtech.inspect.schema.Catalog;
import org.oneedtech.inspect.schema.SchemaKey; import org.oneedtech.inspect.schema.SchemaKey;
import org.oneedtech.inspect.util.resource.Resource; import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.MoreObjects; 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 * Base credential class for OB 2.0 Assertions and OB 3.0 and CLR 2.0 Credentials.
* and the extracted JSON data plus any other stuff Probes need. * This contains e.g. the origin resource and the extracted JSON data.
* @author mgylling * @author xaracil
*/ */
public class Credential extends GeneratedObject { public abstract class Credential extends GeneratedObject {
final Resource resource; final Resource resource;
final JsonNode jsonData; final JsonNode jsonData;
final Credential.Type credentialType;
final String jwt; final String jwt;
final String issuedOnPropertyName;
public Credential(Resource resource, JsonNode data, String jwt) { final String expiresAtPropertyName;
super(ID, GeneratedObject.Type.INTERNAL); final Map<CredentialEnum, SchemaKey> schemas;
protected Credential(String id, Resource resource, JsonNode data, String jwt, Map<CredentialEnum, SchemaKey> schemas, String issuedOnPropertyName, String expiresAtPropertyName) {
super(id, GeneratedObject.Type.INTERNAL);
this.resource = checkNotNull(resource); this.resource = checkNotNull(resource);
this.jsonData = checkNotNull(data); this.jsonData = checkNotNull(data);
this.jwt = jwt; //may be null this.jwt = jwt; //may be null
this.schemas = schemas;
this.issuedOnPropertyName = issuedOnPropertyName;
this.expiresAtPropertyName = expiresAtPropertyName;
checkTrue(RECOGNIZED_PAYLOAD_TYPES.contains(resource.getType())); 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() { public Resource getResource() {
return credentialType; return resource;
} }
public Optional<String> getJwt() { public JsonNode getJson() {
return jsonData;
}
public Optional<String> getJwt() {
return Optional.ofNullable(jwt); return Optional.ofNullable(jwt);
} }
public ProofType getProofType() { public String getIssuedOnPropertyName() {
return jwt == null ? ProofType.EMBEDDED : ProofType.EXTERNAL; return issuedOnPropertyName;
} }
public String getExpiresAtPropertyName() {
private static final Map<Credential.Type, SchemaKey> schemas = new ImmutableMap.Builder<Credential.Type, SchemaKey>() return expiresAtPropertyName;
.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();
/**
* Get the canonical schema for this credential if such exists. * Get the canonical schema for this credential if such exists.
*/ */
public Optional<SchemaKey> getSchemaKey() { public Optional<SchemaKey> getSchemaKey() {
return Optional.ofNullable(schemas.get(credentialType)); return Optional.ofNullable(schemas.get(getCredentialType()));
} }
public enum Type { public abstract CredentialEnum getCredentialType();
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<JsonNode> 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
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("resource", resource.getID()) .add("resource", resource.getID())
.add("resourceType", resource.getType()) .add("resourceType", resource.getType())
.add("credentialType", credentialType)
.add("json", jsonData) .add("json", jsonData)
.add("jwt", jwt)
.toString(); .toString();
} }
public static final String ID = Credential.class.getCanonicalName();
public static final List<ResourceType> RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT); public static final List<ResourceType> RECOGNIZED_PAYLOAD_TYPES = List.of(SVG, PNG, JSON, JWT);
public static final String CREDENTIAL_KEY = "CREDENTIAL_KEY"; public static final String CREDENTIAL_KEY = "CREDENTIAL_KEY";
public interface CredentialEnum {
List<String> getRequiredTypeValues();
List<String> getAllowedTypeValues();
boolean isAllowedTypeValuesRequired();
List<String> getContextUris();
String toString();
}
public abstract static class Builder<B extends Credential> {
private Resource resource;
private JsonNode jsonData;
private String jwt;
public abstract B build();
public Builder<B> resource(Resource resource) {
this.resource = resource;
return this;
}
public Builder<B> jsonData(JsonNode node) {
this.jsonData = node;
return this;
}
public Builder<B> jwt(String jwt) {
this.jwt = jwt;
return this;
}
protected Resource getResource() {
return resource;
}
protected JsonNode getJsonData() {
return jsonData;
}
protected String getJwt() {
return jwt;
}
}
} }

View File

@ -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.code.Defensives.checkNotNull;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; 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.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.net.URI;
import java.util.ArrayList; 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.Resource;
import org.oneedtech.inspect.util.resource.UriResource; import org.oneedtech.inspect.util.resource.UriResource;
import org.oneedtech.inspect.util.resource.context.ResourceContext; 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.ContextPropertyProbe;
import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe; import org.oneedtech.inspect.vc.probe.EmbeddedProofProbe;
import org.oneedtech.inspect.vc.probe.ExpirationProbe; import org.oneedtech.inspect.vc.probe.ExpirationProbe;
@ -39,34 +39,35 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
/** /**
* An inspector for EndorsementCredential objects. * An inspector for EndorsementCredential objects.
* @author mgylling * @author mgylling
*/ */
public class EndorsementInspector extends VCInspector implements SubInspector { public class EndorsementInspector extends VCInspector implements SubInspector {
protected <B extends VCInspector.Builder<?>> EndorsementInspector(B builder) { protected <B extends VCInspector.Builder<?>> EndorsementInspector(B builder) {
super(builder); super(builder);
} }
@Override @Override
public Report run(Resource resource, Map<String, GeneratedObject> parentObjects) { public Report run(Resource resource, Map<String, GeneratedObject> parentObjects) {
/* /*
* The resource param is the top-level credential that embeds the endorsement, we * 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. * 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 * 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); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
RunContext ctx = new RunContext.Builder() RunContext ctx = new RunContext.Builder()
.put(this) .put(this)
.put(resource)
.put(JACKSON_OBJECTMAPPER, mapper) .put(JACKSON_OBJECTMAPPER, mapper)
.put(JSONPATH_EVALUATOR, jsonPath) .put(JSONPATH_EVALUATOR, jsonPath)
.build(); .build();
@ -74,18 +75,18 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
List<ReportItems> accumulator = new ArrayList<>(); List<ReportItems> accumulator = new ArrayList<>();
int probeCount = 0; int probeCount = 0;
try { try {
//context and type properties //context and type properties
Credential.Type type = Type.EndorsementCredential; VerifiableCredential.Type type = Type.EndorsementCredential;
for(Probe<JsonNode> probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { for(Probe<JsonNode> probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
probeCount++; probeCount++;
accumulator.add(probe.run(endorsement.getJson(), ctx)); accumulator.add(probe.run(endorsement.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
//inline schema (parent inspector has already validated against canonical) //inline schema (parent inspector has already validated against canonical)
accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx)); accumulator.add(new InlineJsonSchemaProbe().run(endorsement.getJson(), ctx));
//signatures, proofs //signatures, proofs
probeCount++; probeCount++;
if(endorsement.getProofType() == EXTERNAL){ if(endorsement.getProofType() == EXTERNAL){
@ -93,16 +94,16 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
accumulator.add(new ExternalProofProbe().run(endorsement, ctx)); accumulator.add(new ExternalProofProbe().run(endorsement, ctx));
} else { } else {
//The credential not contained in a jwt, must have an internal proof. //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); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
//check refresh service if we are not already refreshed (check just like in external CLR) //check refresh service if we are not already refreshed (check just like in external CLR)
probeCount++; probeCount++;
if(resource.getContext().get(REFRESHED) != TRUE) { if(resource.getContext().get(REFRESHED) != TRUE) {
Optional<String> newID = checkRefreshService(endorsement, ctx); Optional<String> newID = checkRefreshService(endorsement, ctx);
if(newID.isPresent()) { if(newID.isPresent()) {
//TODO resource.type //TODO resource.type
return this.run( return this.run(
new UriResource(new URI(newID.get())) new UriResource(new URI(newID.get()))
@ -111,8 +112,8 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
} }
//revocation, expiration and issuance //revocation, expiration and issuance
for(Probe<Credential> probe : List.of(new RevocationListProbe(), for(Probe<Credential> probe : List.of(new RevocationListProbe(),
new ExpirationProbe(), new IssuanceProbe())) { new ExpirationProbe(), new IssuanceProbe())) {
probeCount++; probeCount++;
accumulator.add(probe.run(endorsement, ctx)); accumulator.add(probe.run(endorsement, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
@ -129,7 +130,7 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
public <R extends Resource> Report run(R resource) { public <R extends Resource> Report run(R resource) {
throw new IllegalStateException("must use #run(resource, map)"); throw new IllegalStateException("must use #run(resource, map)");
} }
public static class Builder extends VCInspector.Builder<EndorsementInspector.Builder> { public static class Builder extends VCInspector.Builder<EndorsementInspector.Builder> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
@ -137,5 +138,5 @@ public class EndorsementInspector extends VCInspector implements SubInspector {
return new EndorsementInspector(this); return new EndorsementInspector(this);
} }
} }
} }

View File

@ -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<String, GeneratedObject> 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<ReportItems> accumulator = new ArrayList<>();
int probeCount = 0;
try {
JsonNode endorsementNode = endorsement.getJson();
// verification and revocation
if (endorsement.getCredentialType() == Type.Endorsement) {
for(Probe<JsonLdGeneratedObject> 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<Credential> 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 <R extends Resource> Report run(R resource) {
throw new IllegalStateException("must use #run(resource, map)");
}
public static class Builder extends VCInspector.Builder<OB20EndorsementInspector.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;
}
}
}

View File

@ -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 <B extends VCInspector.Builder<?>> 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<ReportItems> 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<JsonNode> 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<Validation> 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<JsonLdGeneratedObject> 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<Credential> 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<JsonNode> 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<Tuple<ExtensionProbe, JsonNode>> extensionProbeTuples = jsonLdGeneratedObjects.stream()
.flatMap(node -> getExtensionProbes(node, "id").stream())
.collect(toList());
for (Tuple<ExtensionProbe, JsonNode> 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<JsonNode> 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<String, GeneratedObject> 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<OB20Inspector.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";
}
}

View File

@ -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.code.Defensives.*;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; 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.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.payload.PayloadParser.fromJwt;
import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asNodeList; 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.UriResource;
import org.oneedtech.inspect.util.resource.context.ResourceContext; import org.oneedtech.inspect.util.resource.context.ResourceContext;
import org.oneedtech.inspect.util.spec.Specification; 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.ContextPropertyProbe;
import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe;
import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe; import org.oneedtech.inspect.vc.probe.CredentialSubjectProbe;
@ -56,112 +58,120 @@ import com.google.common.collect.ImmutableList;
* @author mgylling * @author mgylling
*/ */
public class OB30Inspector extends VCInspector implements SubInspector { public class OB30Inspector extends VCInspector implements SubInspector {
protected final List<Probe<Credential>> userProbes; protected final List<Probe<VerifiableCredential>> userProbes;
protected OB30Inspector(OB30Inspector.Builder builder) { protected OB30Inspector(OB30Inspector.Builder builder) {
super(builder); super(builder);
this.userProbes = ImmutableList.copyOf(builder.probes); this.userProbes = ImmutableList.copyOf(builder.probes);
} }
//https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit# //https://docs.google.com/document/d/1_imUl2K-5tMib0AUxwA9CWb0Ap1b3qif0sXydih68J0/edit#
//https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#verificaton-and-validation //https://imsglobal.github.io/openbadges-specification/ob_v3p0.html#verificaton-and-validation
/* /*
* This inspector supports both standalone openbadge verification, as well as verification of * 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 * 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 @Override
public Report run(Resource resource) { public Report run(Resource resource) {
super.check(resource); //TODO because URIs, this should be a fetch and cache super.check(resource); //TODO because URIs, this should be a fetch and cache
if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) { if(getBehavior(RESET_CACHES_ON_RUN) == TRUE) {
JsonSchemaCache.reset(); JsonSchemaCache.reset();
CachingDocumentLoader.reset(); CachingDocumentLoader.reset();
} }
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
RunContext ctx = new RunContext.Builder() RunContext ctx = new RunContext.Builder()
.put(this) .put(this)
.put(resource) .put(resource)
.put(Key.JACKSON_OBJECTMAPPER, mapper) .put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath) .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<ReportItems> accumulator = new ArrayList<>(); List<ReportItems> accumulator = new ArrayList<>();
int probeCount = 0; int probeCount = 0;
try { try {
//detect type (png, svg, json, jwt) and extract json data //detect type (png, svg, json, jwt) and extract json data
probeCount++; probeCount++;
accumulator.add(new CredentialParseProbe().run(resource, ctx)); accumulator.add(new CredentialParseProbe().run(resource, ctx));
if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount); if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
//we expect the above to place a generated object in the context //we expect the above to place a generated object in the context
Credential ob = ctx.getGeneratedObject(Credential.ID); VerifiableCredential ob = ctx.getGeneratedObject(VerifiableCredential.ID);
//call the subinspector method of this //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(); probeCount += subReport.getSummary().getTotalRun();
accumulator.add(subReport); accumulator.add(subReport);
//finally, run any user-added probes //finally, run any user-added probes
for(Probe<Credential> probe : userProbes) { for(Probe<VerifiableCredential> probe : userProbes) {
probeCount++; probeCount++;
accumulator.add(probe.run(ob, ctx)); accumulator.add(probe.run(ob, ctx));
} }
} catch (Exception e) { } catch (Exception e) {
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, 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 @Override
public Report run(Resource resource, Map<String, GeneratedObject> parentObjects) { public Report run(Resource resource, Map<String, GeneratedObject> parentObjects) {
Credential ob = checkNotNull((Credential)parentObjects.get(CREDENTIAL_KEY)); VerifiableCredential ob = checkNotNull((VerifiableCredential)parentObjects.get(CREDENTIAL_KEY));
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
VerifiableCredential.Builder credentialBuilder = new VerifiableCredential.Builder();
RunContext ctx = new RunContext.Builder() RunContext ctx = new RunContext.Builder()
.put(this) .put(this)
.put(resource) .put(resource)
.put(Key.JACKSON_OBJECTMAPPER, mapper) .put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath) .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(); .build();
List<ReportItems> accumulator = new ArrayList<>(); List<ReportItems> accumulator = new ArrayList<>();
int probeCount = 0; int probeCount = 0;
try { try {
//context and type properties //context and type properties
Credential.Type type = Type.OpenBadgeCredential; VerifiableCredential.Type type = Type.OpenBadgeCredential;
for(Probe<JsonNode> probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) { for(Probe<JsonNode> probe : List.of(new ContextPropertyProbe(type), new TypePropertyProbe(type))) {
probeCount++; probeCount++;
accumulator.add(probe.run(ob.getJson(), ctx)); accumulator.add(probe.run(ob.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
//canonical schema and inline schemata //canonical schema and inline schemata
SchemaKey schema = ob.getSchemaKey().orElseThrow(); SchemaKey schema = ob.getSchemaKey().orElseThrow();
for(Probe<JsonNode> probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) { for(Probe<JsonNode> probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) {
probeCount++; probeCount++;
accumulator.add(probe.run(ob.getJson(), ctx)); accumulator.add(probe.run(ob.getJson(), ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
//credentialSubject //credentialSubject
probeCount++; probeCount++;
accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx)); accumulator.add(new CredentialSubjectProbe().run(ob.getJson(), ctx));
//signatures, proofs //signatures, proofs
probeCount++; probeCount++;
if(ob.getProofType() == EXTERNAL){ if(ob.getProofType() == EXTERNAL){
@ -169,57 +179,57 @@ public class OB30Inspector extends VCInspector implements SubInspector {
accumulator.add(new ExternalProofProbe().run(ob, ctx)); accumulator.add(new ExternalProofProbe().run(ob, ctx));
} else { } else {
//The credential not contained in a jwt, must have an internal proof. //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); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
//check refresh service if we are not already refreshed //check refresh service if we are not already refreshed
probeCount++; probeCount++;
if(resource.getContext().get(REFRESHED) != TRUE) { if(resource.getContext().get(REFRESHED) != TRUE) {
Optional<String> newID = checkRefreshService(ob, ctx); Optional<String> newID = checkRefreshService(ob, ctx);
if(newID.isPresent()) { if(newID.isPresent()) {
return this.run( return this.run(
new UriResource(new URI(newID.get())) new UriResource(new URI(newID.get()))
.setContext(new ResourceContext(REFRESHED, TRUE))); .setContext(new ResourceContext(REFRESHED, TRUE)));
} }
} }
//revocation, expiration and issuance //revocation, expiration and issuance
for(Probe<Credential> probe : List.of(new RevocationListProbe(), for(Probe<Credential> probe : List.of(new RevocationListProbe(),
new ExpirationProbe(), new IssuanceProbe())) { new ExpirationProbe(), new IssuanceProbe())) {
probeCount++; probeCount++;
accumulator.add(probe.run(ob, ctx)); accumulator.add(probe.run(ob, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
//embedded endorsements //embedded endorsements
EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build(); EndorsementInspector endorsementInspector = new EndorsementInspector.Builder().build();
List<JsonNode> endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath); List<JsonNode> endorsements = asNodeList(ob.getJson(), "$..endorsement", jsonPath);
for(JsonNode node : endorsements) { for(JsonNode node : endorsements) {
probeCount++; 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))); accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
} }
//embedded jwt endorsements //embedded jwt endorsements
endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath); endorsements = asNodeList(ob.getJson(), "$..endorsementJwt", jsonPath);
for(JsonNode node : endorsements) { for(JsonNode node : endorsements) {
probeCount++; probeCount++;
String jwt = node.asText(); String jwt = node.asText();
JsonNode vcNode = fromJwt(jwt, ctx); 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))); accumulator.add(endorsementInspector.run(resource, Map.of(CREDENTIAL_KEY, endorsement)));
} }
} catch (Exception e) { } catch (Exception e) {
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, 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<OB30Inspector.Builder> { public static class Builder extends VCInspector.Builder<OB30Inspector.Builder> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
@ -228,5 +238,5 @@ public class OB30Inspector extends VCInspector implements SubInspector {
set(ResourceType.OPENBADGE); set(ResourceType.OPENBADGE);
return new OB30Inspector(this); return new OB30Inspector(this);
} }
} }
} }

View File

@ -1,8 +1,16 @@
package org.oneedtech.inspect.vc; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; 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.Inspector;
import org.oneedtech.inspect.core.probe.Outcome; 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.probe.RunContext;
import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.Report;
import org.oneedtech.inspect.core.report.ReportItems; 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; import com.fasterxml.jackson.databind.JsonNode;
/** /**
@ -18,19 +33,19 @@ import com.fasterxml.jackson.databind.JsonNode;
* @author mgylling * @author mgylling
*/ */
public abstract class VCInspector extends Inspector { public abstract class VCInspector extends Inspector {
protected <B extends VCInspector.Builder<?>> VCInspector(B builder) { protected <B extends VCInspector.Builder<?>> VCInspector(B builder) {
super(builder); super(builder);
} }
protected Report abort(RunContext ctx, List<ReportItems> accumulator, int probeCount) { protected Report abort(RunContext ctx, List<ReportItems> accumulator, int probeCount) {
return new Report(ctx, new ReportItems(accumulator), probeCount); return new Report(ctx, new ReportItems(accumulator), probeCount);
} }
protected boolean broken(List<ReportItems> accumulator) { protected boolean broken(List<ReportItems> accumulator) {
return broken(accumulator, false); return broken(accumulator, false);
} }
protected boolean broken(List<ReportItems> accumulator, boolean force) { protected boolean broken(List<ReportItems> accumulator, boolean force) {
if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) { if(!force && getBehavior(Inspector.Behavior.VALIDATOR_FAIL_FAST) == Boolean.FALSE) {
return false; return false;
@ -40,15 +55,15 @@ public abstract class VCInspector extends Inspector {
} }
return false; return false;
} }
/** /**
* If the AchievementCredential or EndorsementCredential has a refreshService property and the type of the * 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 * 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, * provided, then start the verification process over using the response as input. If the request fails,
* the credential is invalid. * the credential is invalid.
*/ */
protected Optional<String> checkRefreshService(Credential crd, RunContext ctx) { protected Optional<String> checkRefreshService(VerifiableCredential crd, RunContext ctx) {
JsonNode refreshServiceNode = crd.getJson().get("refreshService"); JsonNode refreshServiceNode = crd.getJson().get("refreshService");
if(refreshServiceNode != null) { if(refreshServiceNode != null) {
JsonNode serviceTypeNode = refreshServiceNode.get("type"); JsonNode serviceTypeNode = refreshServiceNode.get("type");
if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) { if(serviceTypeNode != null && serviceTypeNode.asText().equals("1EdTechCredentialRefresh")) {
@ -56,22 +71,74 @@ public abstract class VCInspector extends Inspector {
if(serviceURINode != null) { if(serviceURINode != null) {
return Optional.of(serviceURINode.asText()); return Optional.of(serviceURINode.asText());
} }
} }
} }
return Optional.empty(); 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<Tuple<ExtensionProbe, JsonNode>> getExtensionProbes(JsonNode node, String entryPath) {
List<Tuple<ExtensionProbe, JsonNode>> probes = new ArrayList<>();
if (!node.isObject()) {
return probes;
}
if (node.has("type")) {
List<String> types = asStringList(node.get("type"));
// only validate extension types
if (types.contains("Extension")) {
List<String> typesToTest = types.stream().filter(type -> !type.equals("Extension")).collect(toList());
// add an extension Probe
probes.add(new Tuple<ExtensionProbe,JsonNode>(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<JsonNode> childNodes = JsonNodeUtil.asNodeList(entry.getValue());
List<Tuple<ExtensionProbe, JsonNode>> 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"; protected static final String REFRESHED = "is.refreshed.credential";
public abstract static class Builder<B extends VCInspector.Builder<B>> extends Inspector.Builder<B> { public abstract static class Builder<B extends VCInspector.Builder<B>> extends Inspector.Builder<B> {
final List<Probe<Credential>> probes; final List<Probe<VerifiableCredential>> probes;
public Builder() { public Builder() {
super(); super();
this.probes = new ArrayList<>(); this.probes = new ArrayList<>();
} }
public VCInspector.Builder<B> add(Probe<Credential> probe) { public VCInspector.Builder<B> add(Probe<VerifiableCredential> probe) {
probes.add(probe); probes.add(probe);
return this; return this;
} }

View File

@ -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<String> mustContainOne;
private final List<Validation> prerequisites;
private final List<Assertion.Type> 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<String> getMustContainOne() {
return mustContainOne;
}
public List<Validation> getPrerequisites() {
return prerequisites;
}
public List<Assertion.Type> 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<String> mustContainOne;
private List<Validation> prerequisites;
private List<Assertion.Type> 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<String> elems) {
this.mustContainOne = elems;
return this;
}
public Builder mustContainOneType(List<Assertion.Type> types) {
this.mustContainOne = types.stream().map(Assertion.Type::toString).collect(Collectors.toList());
return this;
}
public Builder prerequisites(List<Validation> elems) {
this.prerequisites = elems;
return this;
}
public Builder prerequisite(Validation elem) {
this.prerequisites = List.of(elem);
return this;
}
public Builder expectedTypes(List<Assertion.Type> 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);
}
}
}

View File

@ -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<CredentialEnum, SchemaKey> 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<CredentialEnum, SchemaKey> schemas = new ImmutableMap.Builder<CredentialEnum, SchemaKey>()
.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<Set<VerifiableCredential.Type>, List<String>> contextMap = new ImmutableMap.Builder<Set<VerifiableCredential.Type>, List<String>>()
.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<String> allowedTypeValues;
Type(List<String> allowedTypeValues) {
this.allowedTypeValues = allowedTypeValues;
}
public static VerifiableCredential.Type valueOf (JsonNode typeNode) {
if(typeNode != null) {
List<String> 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<String> getRequiredTypeValues() {
return List.of("VerifiableCredential");
}
@Override
public List<String> getAllowedTypeValues() {
return allowedTypeValues;
}
@Override
public boolean isAllowedTypeValuesRequired() {
return true;
}
@Override
public List<String> 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<VerifiableCredential> {
@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";
}

View File

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

View File

@ -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<JsonNode> {
private final List<String> typesToTest;
public ExtensionProbe(String entryPath, List<String> 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<URI> 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<JsonNode> 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<JsonNode> 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";
}

View File

@ -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<JsonNode> {
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<Validation> 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<JsonNode> 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);
}

View File

@ -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<Credential> {
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();
}

View File

@ -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<Credential> {
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);
}
}
}

View File

@ -4,10 +4,10 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import static org.oneedtech.inspect.util.code.Defensives.checkTrue; import static org.oneedtech.inspect.util.code.Defensives.checkTrue;
import org.oneedtech.inspect.core.probe.RunContext; 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.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
/** /**
@ -22,11 +22,15 @@ public final class JsonParser extends PayloadParser {
} }
@Override @Override
public Credential parse(Resource resource, RunContext ctx) throws Exception { public Credential parse(Resource resource, RunContext ctx) throws Exception {
checkTrue(resource.getType() == ResourceType.JSON); checkTrue(resource.getType() == ResourceType.JSON);
String json = resource.asByteSource().asCharSource(UTF_8).read(); String json = resource.asByteSource().asCharSource(UTF_8).read();
JsonNode node = fromString(json, ctx); JsonNode node = fromString(json, ctx);
return new Credential(resource, node);
return getBuilder(ctx)
.resource(resource)
.jsonData(node)
.build();
} }
} }

View File

@ -22,11 +22,15 @@ public final class JwtParser extends PayloadParser {
} }
@Override @Override
public Credential parse(Resource resource, RunContext ctx) throws Exception { public Credential parse(Resource resource, RunContext ctx) throws Exception {
checkTrue(resource.getType() == ResourceType.JWT); checkTrue(resource.getType() == ResourceType.JWT);
String jwt = resource.asByteSource().asCharSource(UTF_8).read(); String jwt = resource.asByteSource().asCharSource(UTF_8).read();
JsonNode node = fromJwt(jwt, ctx); JsonNode node = fromJwt(jwt, ctx);
return new Credential(resource, node, jwt); return getBuilder(ctx)
.resource(resource)
.jsonData(node)
.jwt(jwt)
.build();
} }
} }

View File

@ -1,10 +1,13 @@
package org.oneedtech.inspect.vc.payload; package org.oneedtech.inspect.vc.payload;
import static com.apicatalog.jsonld.StringUtils.isBlank;
import java.util.Base64; import java.util.Base64;
import java.util.List;
import java.util.Base64.Decoder; import java.util.Base64.Decoder;
import java.util.List;
import org.oneedtech.inspect.core.probe.RunContext; 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.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential;
@ -20,13 +23,18 @@ import com.google.common.base.Splitter;
public abstract class PayloadParser { public abstract class PayloadParser {
public abstract boolean supports(ResourceType type); public abstract boolean supports(ResourceType type);
public abstract Credential parse(Resource source, RunContext ctx) throws Exception; 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 { protected static JsonNode fromString(String json, RunContext context) throws Exception {
return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json); return ((ObjectMapper)context.get(RunContext.Key.JACKSON_OBJECTMAPPER)).readTree(json);
} }
/** /**
* Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof * Decode as per https://www.imsglobal.org/spec/ob/v3p0/#jwt-proof
* @return The decoded JSON String * @return The decoded JSON String
@ -34,19 +42,23 @@ public abstract class PayloadParser {
public static JsonNode fromJwt(String jwt, RunContext context) throws Exception { public static JsonNode fromJwt(String jwt, RunContext context) throws Exception {
List<String> parts = Splitter.on('.').splitToList(jwt); List<String> parts = Splitter.on('.').splitToList(jwt);
if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
final Decoder decoder = Base64.getUrlDecoder(); final Decoder decoder = Base64.getUrlDecoder();
/* /*
* For this step we are only deserializing the stored badge out of the payload. * 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))); 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 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; return vcNode;
} }
} }

View File

@ -30,11 +30,12 @@ public final class PngParser extends PayloadParser {
@Override @Override
public Credential parse(Resource resource, RunContext ctx) throws Exception { public Credential parse(Resource resource, RunContext ctx) throws Exception {
checkTrue(resource.getType() == ResourceType.PNG); checkTrue(resource.getType() == ResourceType.PNG);
try(InputStream is = resource.asByteSource().openStream()) { try(InputStream is = resource.asByteSource().openStream()) {
final Keys credentialKey = (Keys) ctx.get(RunContext.Key.PNG_CREDENTIAL_KEY);
ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next(); ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next();
imageReader.setInput(ImageIO.createImageInputStream(is), true); imageReader.setInput(ImageIO.createImageInputStream(is), true);
IIOMetadata metadata = imageReader.getImageMetadata(0); IIOMetadata metadata = imageReader.getImageMetadata(0);
@ -43,20 +44,20 @@ public final class PngParser extends PayloadParser {
String jwtString = null; String jwtString = null;
String formatSearch = null; String formatSearch = null;
JsonNode vcNode = null; JsonNode vcNode = null;
String[] names = metadata.getMetadataFormatNames(); String[] names = metadata.getMetadataFormatNames();
int length = names.length; int length = names.length;
for (int i = 0; i < length; i++) { 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") //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])); formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]), credentialKey);
if(formatSearch != null) { if(formatSearch != null) {
vcString = formatSearch; vcString = formatSearch;
break; break;
} }
} }
if(vcString == null) { if(vcString == null) {
throw new IllegalArgumentException("No credential inside PNG"); throw new IllegalArgumentException("No credential inside PNG");
} }
vcString = vcString.trim(); vcString = vcString.trim();
@ -68,29 +69,33 @@ public final class PngParser extends PayloadParser {
else { else {
vcNode = fromString(vcString, ctx); 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(); NamedNodeMap attributes = node.getAttributes();
//If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one. //If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one.
Node keyword = attributes.getNamedItem("keyword"); 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"); Node textAttribute = attributes.getNamedItem("text");
if(textAttribute != null) { if(textAttribute != null) {
return textAttribute.getNodeValue(); return textAttribute.getNodeValue();
} }
} }
//iterate over all children depth first and search for the credential node. //iterate over all children depth first and search for the credential node.
Node child = node.getFirstChild(); Node child = node.getFirstChild();
while (child != null) { while (child != null) {
String nodeValue = getOpenBadgeCredentialNodeText(child); String nodeValue = getOpenBadgeCredentialNodeText(child, credentialKey);
if(nodeValue != null) { if(nodeValue != null) {
return nodeValue; return nodeValue;
} }
child = child.getNextSibling(); child = child.getNextSibling();
} }
@ -99,4 +104,19 @@ public final class PngParser extends PayloadParser {
return null; 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;
}
}
} }

View File

@ -11,7 +11,6 @@ import javax.xml.stream.events.Characters;
import javax.xml.stream.events.XMLEvent; import javax.xml.stream.events.XMLEvent;
import org.oneedtech.inspect.core.probe.RunContext; 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.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.util.xml.XMLInputFactoryCache; import org.oneedtech.inspect.util.xml.XMLInputFactoryCache;
@ -32,48 +31,74 @@ public final class SvgParser extends PayloadParser {
@Override @Override
public Credential parse(Resource resource, RunContext ctx) throws Exception { 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); checkTrue(resource.getType() == ResourceType.SVG);
try(InputStream is = resource.asByteSource().openStream()) { try(InputStream is = resource.asByteSource().openStream()) {
XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is); XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is);
while(reader.hasNext()) { while(reader.hasNext()) {
XMLEvent ev = reader.nextEvent(); XMLEvent ev = reader.nextEvent();
if(isEndElem(ev, OB_CRED_ELEM)) break; if(isEndElem(ev, qNames.getCredentialElem())) break;
if(isStartElem(ev, OB_CRED_ELEM)) { if(isStartElem(ev, qNames.getCredentialElem())) {
Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR); Attribute verifyAttr = ev.asStartElement().getAttributeByName(qNames.getVerifyElem());
if(verifyAttr != null) { if(verifyAttr != null) {
String jwt = verifyAttr.getValue(); String jwt = verifyAttr.getValue();
JsonNode node = fromJwt(jwt, ctx); JsonNode node = fromJwt(jwt, ctx);
return new Credential(resource, node, jwt); return getBuilder(ctx).resource(resource).jsonData(node).jwt(jwt).build();
} else { } else {
while(reader.hasNext()) { while(reader.hasNext()) {
ev = reader.nextEvent(); ev = reader.nextEvent();
if(isEndElem(ev, OB_CRED_ELEM)) break; if(isEndElem(ev, qNames.getCredentialElem())) break;
if(ev.getEventType() == XMLEvent.CHARACTERS) { if(ev.getEventType() == XMLEvent.CHARACTERS) {
Characters chars = ev.asCharacters(); Characters chars = ev.asCharacters();
if(!chars.isWhiteSpace()) { if(!chars.isWhiteSpace()) {
JsonNode node = fromString(chars.getData(), ctx); JsonNode node = fromString(chars.getData(), ctx);
return new Credential(resource, node); return getBuilder(ctx).resource(resource).jsonData(node).build();
} }
} }
} }
} }
} }
} //while(reader.hasNext()) { } //while(reader.hasNext()) {
} }
throw new IllegalArgumentException("No credential inside SVG"); throw new IllegalArgumentException("No credential inside SVG");
} }
private boolean isEndElem(XMLEvent ev, QName name) { private boolean isEndElem(XMLEvent ev, QName name) {
return ev.isEndElement() && ev.asEndElement().getName().equals(name); return ev.isEndElement() && ev.asEndElement().getName().equals(name);
} }
private boolean isStartElem(XMLEvent ev, QName name) { private boolean isStartElem(XMLEvent ev, QName name) {
return ev.isStartElement() && ev.asStartElement().getName().equals(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"); 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;
}
}
} }

View File

@ -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<JsonLdGeneratedObject> {
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<JsonNode> revocationList = JsonNodeUtil.asNodeList(revocationListNode.get("revokedAssertions"));
List<JsonNode> 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<JsonNode> 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();
}

View File

@ -1,76 +1,50 @@
package org.oneedtech.inspect.vc.probe; package org.oneedtech.inspect.vc.probe;
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.checkNotNull;
import java.util.List; 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.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential.CredentialEnum;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.fasterxml.jackson.databind.JsonNode; 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. * A Probe that verifies a credential's context property.
* *
* @author mgylling * @author mgylling
*/ */
public class ContextPropertyProbe extends Probe<JsonNode> { public class ContextPropertyProbe extends StringValuePropertyProbe {
private final Credential.Type type; private final CredentialEnum type;
public ContextPropertyProbe(Credential.Type type) { public ContextPropertyProbe(CredentialEnum type) {
super(ID); super(ID, type.toString(), "@context");
this.type = checkNotNull(type); this.type = checkNotNull(type);
setValueValidations(this::validate);
} }
@Override @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"); public ReportItems validate(List<String> nodeValues, RunContext ctx) {
if (contextNode == null) { if (!nodeValues.isEmpty()) { // empty context uri node: inline context
return notRun("No @context property", ctx); List<String> contextUris = type.getContextUris();
} checkNotNull(contextUris);
List<String> expected = values.get(values.keySet() int pos = 0;
.stream() for (String uri : contextUris) {
.filter(s->s.contains(type)) if ((nodeValues.size() < pos + 1) || !nodeValues.get(pos).equals(uri)) {
.findFirst() return error("missing required @context uri " + uri + " at position " + (pos + 1), ctx);
.orElseThrow(()-> new IllegalArgumentException(type.name() + " not recognized"))); }
pos++;
List<String> 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);
} }
pos++;
} }
return success(ctx); return success(ctx);
} }
private final static Map<Set<Credential.Type>, List<String>> values = new ImmutableMap.Builder<Set<Credential.Type>, List<String>>()
.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(); public static final String ID = ContextPropertyProbe.class.getSimpleName();
} }

View File

@ -9,25 +9,26 @@ import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.util.resource.detect.TypeDetector; import org.oneedtech.inspect.util.resource.detect.TypeDetector;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential;
import org.oneedtech.inspect.vc.VerifiableCredential;
import org.oneedtech.inspect.vc.payload.PayloadParserFactory; import org.oneedtech.inspect.vc.payload.PayloadParserFactory;
/** /**
* A probe that verifies that the incoming credential resource is of a recognized payload type * 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) * and if so extracts and stores the VC json data (a 'Credential' instance)
* in the RunContext. * in the RunContext.
* @author mgylling * @author mgylling
*/ */
public class CredentialParseProbe extends Probe<Resource> { public class CredentialParseProbe extends Probe<Resource> {
@Override @Override
public ReportItems run(Resource resource, RunContext context) throws Exception { public ReportItems run(Resource resource, RunContext context) throws Exception {
try { try {
//TODO if .detect reads from a URIResource twice. Cache the resource on first call. //TODO if .detect reads from a URIResource twice. Cache the resource on first call.
Optional<ResourceType> type = Optional.ofNullable(resource.getType()); Optional<ResourceType> type = Optional.ofNullable(resource.getType());
if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) { if(type.isEmpty() || type.get() == ResourceType.UNKNOWN) {
type = TypeDetector.detect(resource, true); type = TypeDetector.detect(resource, true);
if(type.isEmpty()) { if(type.isEmpty()) {
//TODO if URI fetch, TypeDetector likely to fail //TODO if URI fetch, TypeDetector likely to fail
@ -37,18 +38,18 @@ public class CredentialParseProbe extends Probe<Resource> {
resource.setType(type.get()); 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); 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); context.addGeneratedObject(crd);
return success(this, context); return success(this, context);
} catch (Exception e) { } catch (Exception e) {
return fatal("Error while parsing credential: " + e.getMessage(), context); return fatal("Error while parsing credential: " + e.getMessage(), context);
} }
} }
} }

View File

@ -7,7 +7,8 @@ import java.util.Optional;
import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; 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.StringUtils;
import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.Document;
@ -15,9 +16,7 @@ import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.apicatalog.multibase.Multibase; import com.apicatalog.multibase.Multibase;
import com.apicatalog.multicodec.Multicodec; import com.apicatalog.multicodec.Multicodec;
import com.apicatalog.multicodec.Multicodec.Codec; 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.LdProof;
import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier; import info.weboftrust.ldsignatures.verifier.Ed25519Signature2020LdVerifier;
import jakarta.json.JsonObject; import jakarta.json.JsonObject;
@ -28,7 +27,7 @@ import jakarta.json.JsonStructure;
* *
* @author mgylling * @author mgylling
*/ */
public class EmbeddedProofProbe extends Probe<Credential> { public class EmbeddedProofProbe extends Probe<VerifiableCredential> {
public EmbeddedProofProbe() { public EmbeddedProofProbe() {
super(ID); super(ID);
@ -39,15 +38,12 @@ public class EmbeddedProofProbe extends Probe<Credential> {
* (https://github.com/danubetech/verifiable-credentials-java) * (https://github.com/danubetech/verifiable-credentials-java)
*/ */
@Override @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? // TODO: What there are multiple proofs?
VerifiableCredential vc = VerifiableCredential.fromJson(new StringReader(crd.getJson().toString())); com.danubetech.verifiablecredentials.VerifiableCredential vc = com.danubetech.verifiablecredentials.VerifiableCredential.fromJson(new StringReader(crd.getJson().toString()));
ConfigurableDocumentLoader documentLoader = new ConfigurableDocumentLoader(); vc.setDocumentLoader(new CachingDocumentLoader());
documentLoader.setEnableHttp(true);
documentLoader.setEnableHttps(true);
vc.setDocumentLoader(documentLoader);
LdProof proof = vc.getLdProof(); LdProof proof = vc.getLdProof();
if (proof == null) { if (proof == null) {
@ -97,7 +93,7 @@ public class EmbeddedProofProbe extends Probe<Credential> {
if (keyStructure.isEmpty()) { if (keyStructure.isEmpty()) {
return error("Key document not found at " + method, ctx); return error("Key document not found at " + method, ctx);
} }
// First look for a Ed25519VerificationKey2020 document // First look for a Ed25519VerificationKey2020 document
controller = keyStructure.get().asJsonObject().getString("controller"); controller = keyStructure.get().asJsonObject().getString("controller");
if (StringUtils.isBlank(controller)) { if (StringUtils.isBlank(controller)) {
@ -113,7 +109,7 @@ public class EmbeddedProofProbe extends Probe<Credential> {
} else { } else {
publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase"); publicKeyMultibase = keyStructure.get().asJsonObject().getString("publicKeyMultibase");
} }
} catch (Exception e) { } catch (Exception e) {
return error("Invalid verification key URL: " + e.getMessage(), ctx); return error("Invalid verification key URL: " + e.getMessage(), ctx);
} }

View File

@ -10,34 +10,34 @@ import org.oneedtech.inspect.vc.Credential;
import com.fasterxml.jackson.databind.JsonNode; 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 * @author mgylling
*/ */
public class ExpirationProbe extends Probe<Credential> { public class ExpirationProbe extends Probe<Credential> {
public ExpirationProbe() { public ExpirationProbe() {
super(ID); super(ID);
} }
@Override @Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception { public ReportItems run(Credential crd, RunContext ctx) throws Exception {
/* /*
* If the AchievementCredential or EndorsementCredential has an expirationDate property * If the AchievementCredential or EndorsementCredential has an expirationDate property
* and the expiration date is prior to the current date, the credential has expired. * and the expiration date is prior to the current date, the credential has expired.
*/ */
JsonNode node = crd.getJson().get("expirationDate"); JsonNode node = crd.getJson().get(crd.getExpiresAtPropertyName());
if(node != null) { if(node != null) {
try { try {
ZonedDateTime expirationDate = ZonedDateTime.parse(node.textValue()); ZonedDateTime expirationDate = ZonedDateTime.parse(node.textValue());
if (ZonedDateTime.now().isAfter(expirationDate)) { if (ZonedDateTime.now().isAfter(expirationDate)) {
return fatal("The credential has expired (expiration date was " + node.asText() + ").", ctx); return fatal("The credential has expired (expiration date was " + node.asText() + ").", ctx);
} }
} catch (Exception e) { } 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); return success(ctx);
} }
public static final String ID = ExpirationProbe.class.getSimpleName(); public static final String ID = ExpirationProbe.class.getSimpleName();
} }

View File

@ -21,7 +21,7 @@ import org.apache.http.util.EntityUtils;
import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; 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.JWT;
import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.JWTVerifier;
@ -36,32 +36,32 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
/** /**
* A Probe that verifies credential external proof (jwt) * A Probe that verifies credential external proof (jwt)
* @author mlyon * @author mlyon
*/ */
public class ExternalProofProbe extends Probe<Credential> { public class ExternalProofProbe extends Probe<VerifiableCredential> {
public ExternalProofProbe() { public ExternalProofProbe() {
super(ID); super(ID);
} }
@Override @Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception { public ReportItems run(VerifiableCredential crd, RunContext ctx) throws Exception {
try { try {
verifySignature(crd, ctx); verifySignature(crd, ctx);
} catch (Exception e) { } catch (Exception e) {
return fatal("Error verifying jwt signature: " + e.getMessage(), ctx); return fatal("Error verifying jwt signature: " + e.getMessage(), ctx);
} }
return success(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().isPresent(), "no jwt supplied");
checkTrue(crd.getJwt().get().length() > 0, "no jwt supplied"); checkTrue(crd.getJwt().get().length() > 0, "no jwt supplied");
DecodedJWT decodedJwt = null; DecodedJWT decodedJwt = null;
String jwt = crd.getJwt().get(); String jwt = crd.getJwt().get();
List<String> parts = Splitter.on('.').splitToList(jwt); List<String> parts = Splitter.on('.').splitToList(jwt);
if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
@ -85,7 +85,7 @@ public class ExternalProofProbe extends Probe<Credential> {
if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); } if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); }
if(kid != null){ 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. //TODO Consider additional testing.
String kidUrl = kid.textValue(); String kidUrl = kid.textValue();
String jwkResponse = fetchJwk(kidUrl); String jwkResponse = fetchJwk(kidUrl);
@ -145,7 +145,7 @@ public class ExternalProofProbe extends Probe<Credential> {
return responseString; return responseString;
} }
public static final String ID = ExternalProofProbe.class.getSimpleName(); public static final String ID = ExternalProofProbe.class.getSimpleName();
} }

View File

@ -11,64 +11,64 @@ import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe; import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.schema.SchemaKey; 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.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode; 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 * @author mgylling
*/ */
public class InlineJsonSchemaProbe extends Probe<JsonNode> { public class InlineJsonSchemaProbe extends Probe<JsonNode> {
private static final Set<String> types = Set.of("1EdTechJsonSchemaValidator2019"); private static final Set<String> types = Set.of("1EdTechJsonSchemaValidator2019");
private SchemaKey skip; private SchemaKey skip;
public InlineJsonSchemaProbe() { public InlineJsonSchemaProbe() {
super(ID); super(ID);
} }
public InlineJsonSchemaProbe(SchemaKey skip) { public InlineJsonSchemaProbe(SchemaKey skip) {
super(ID); super(ID);
this.skip = skip; this.skip = skip;
} }
@Override @Override
public ReportItems run(JsonNode root, RunContext ctx) throws Exception { public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
List<ReportItems> accumulator = new ArrayList<>(); List<ReportItems> accumulator = new ArrayList<>();
Set<String> ioErrors = new HashSet<>(); Set<String> 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"); JsonNode credentialSchemaNode = root.get("credentialSchema");
if(credentialSchemaNode == null) return success(ctx); if(credentialSchemaNode == null) return success(ctx);
ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast ArrayNode schemas = (ArrayNode) credentialSchemaNode; //TODO guard this cast
for(JsonNode schemaNode : schemas) { for(JsonNode schemaNode : schemas) {
JsonNode typeNode = schemaNode.get("type"); JsonNode typeNode = schemaNode.get("type");
if(typeNode == null || !types.contains(typeNode.asText())) continue; if(typeNode == null || !types.contains(typeNode.asText())) continue;
JsonNode idNode = schemaNode.get("id"); JsonNode idNode = schemaNode.get("id");
if(idNode == null) continue; if(idNode == null) continue;
String id = idNode.asText().strip(); String id = idNode.asText().strip();
if(ioErrors.contains(id)) continue; if(ioErrors.contains(id)) continue;
if(equals(skip, id)) continue; if(equals(skip, id)) continue;
try { try {
accumulator.add(new JsonSchemaProbe(id).run(root, ctx)); accumulator.add(new JsonSchemaProbe(id).run(root, ctx));
} catch (Exception e) { } catch (Exception e) {
if(!ioErrors.contains(id)) { if(!ioErrors.contains(id)) {
ioErrors.add(id); ioErrors.add(id);
accumulator.add(error("Could not read schema resource " + id, ctx)); accumulator.add(error("Could not read schema resource " + id, ctx));
} }
} }
} }
return new ReportItems(accumulator); return new ReportItems(accumulator);
} }
private boolean equals(SchemaKey key, String id) { private boolean equals(SchemaKey key, String id) {
if(key == null) return false; if(key == null) return false;
return key.getCanonicalURI().equals(id); return key.getCanonicalURI().equals(id);
} }
public static final String ID = InlineJsonSchemaProbe.class.getSimpleName(); public static final String ID = InlineJsonSchemaProbe.class.getSimpleName();
} }

View File

@ -10,34 +10,34 @@ import org.oneedtech.inspect.vc.Credential;
import com.fasterxml.jackson.databind.JsonNode; 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 * @author mgylling
*/ */
public class IssuanceProbe extends Probe<Credential> { public class IssuanceProbe extends Probe<Credential> {
public IssuanceProbe() { public IssuanceProbe() {
super(ID); super(ID);
} }
@Override @Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception { 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. * 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) { if(node != null) {
try { try {
ZonedDateTime issuanceDate = ZonedDateTime.parse(node.textValue()); ZonedDateTime issuanceDate = ZonedDateTime.parse(node.textValue());
if (issuanceDate.isAfter(ZonedDateTime.now())) { if (issuanceDate.isAfter(ZonedDateTime.now())) {
return fatal("The credential is not yet issued (issuance date is " + node.asText() + ").", ctx); return fatal("The credential is not yet issued (issuance date is " + node.asText() + ").", ctx);
} }
} catch (Exception e) { } catch (Exception e) {
return exception("Error while checking issuanceDate: " + e.getMessage(), ctx.getResource()); return exception("Error while checking issuanceDate: " + e.getMessage(), ctx.getResource());
} }
} }
return success(ctx); return success(ctx);
} }
public static final String ID = IssuanceProbe.class.getSimpleName(); public static final String ID = IssuanceProbe.class.getSimpleName();
} }

View File

@ -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<JsonNode> {
private final String propertyName;
private BiFunction<JsonNode, RunContext, ReportItems> validations;
public PropertyProbe(String id, String typeName, String propertyName) {
super(id, typeName, propertyName);
this.propertyName = propertyName;
this.validations = this::defaultValidation;
}
public void setValidations(BiFunction<JsonNode, RunContext, ReportItems> 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);
}
}

View File

@ -11,6 +11,7 @@ import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential;
import org.oneedtech.inspect.vc.VerifiableCredential;
import org.oneedtech.inspect.vc.util.JsonNodeUtil; import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@ -21,23 +22,23 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* @author mgylling * @author mgylling
*/ */
public class RevocationListProbe extends Probe<Credential> { public class RevocationListProbe extends Probe<Credential> {
public RevocationListProbe() { public RevocationListProbe() {
super(ID); super(ID);
} }
@Override @Override
public ReportItems run(Credential crd, RunContext ctx) throws Exception { public ReportItems run(Credential crd, RunContext ctx) throws Exception {
/* /*
* If the AchievementCredential or EndorsementCredential has a credentialStatus property * If the AchievementCredential or EndorsementCredential has a credentialStatus property
* and the type of the CredentialStatus object is 1EdTechRevocationList, fetch the * and the type of the CredentialStatus object is 1EdTechRevocationList, fetch the
* credential status from the URL provided. If the request is unsuccessful, * credential status from the URL provided. If the request is unsuccessful,
* report a warning, not an error. * report a warning, not an error.
*/ */
JsonNode credentialStatus = crd.getJson().get("credentialStatus"); JsonNode credentialStatus = crd.getJson().get("credentialStatus");
if(credentialStatus != null) { if(credentialStatus != null) {
JsonNode type = credentialStatus.get("type"); JsonNode type = credentialStatus.get("type");
if(type != null && type.asText().strip().equals("1EdTechRevocationList")) { if(type != null && type.asText().strip().equals("1EdTechRevocationList")) {
JsonNode listID = credentialStatus.get("id"); JsonNode listID = credentialStatus.get("id");
@ -46,34 +47,34 @@ public class RevocationListProbe extends Probe<Credential> {
URL url = new URI(listID.asText().strip()).toURL(); URL url = new URI(listID.asText().strip()).toURL();
try (InputStream is = url.openStream()) { try (InputStream is = url.openStream()) {
JsonNode revocList = ((ObjectMapper)ctx.get(JACKSON_OBJECTMAPPER)).readTree(is.readAllBytes()); 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 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 * 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 * credential's id is in the list of revokedCredentials and the value of
* revoked is true or ommitted, the issuer has revoked the credential. */ * revoked is true or ommitted, the issuer has revoked the credential. */
JsonNode crdID = crd.getJson().get("id"); //TODO these != checks sb removed (trigger warning) JsonNode crdID = crd.getJson().get("id"); //TODO these != checks sb removed (trigger warning)
if(crdID != null) { if(crdID != null) {
List<JsonNode> list = JsonNodeUtil.asNodeList(revocList.get("revokedCredentials")); List<JsonNode> list = JsonNodeUtil.asNodeList(revocList.get("revokedCredentials"));
if(list != null) { if(list != null) {
for(JsonNode item : list) { for(JsonNode item : list) {
JsonNode revID = item.get("id"); 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())) { 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) { } catch (Exception e) {
return warning("Error when fetching credentialStatus resource " + e.getMessage(), ctx); return warning("Error when fetching credentialStatus resource " + e.getMessage(), ctx);
} }
} }
} }
} }
return success(ctx); return success(ctx);
} }
public static final String ID = RevocationListProbe.class.getSimpleName(); public static final String ID = RevocationListProbe.class.getSimpleName();
} }

View File

@ -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<List<String>, 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<List<String>, RunContext, ReportItems> validations) {
this.valueValidations = validations;
}
private ReportItems nodeValidation(JsonNode node, RunContext ctx) {
List<String> values = JsonNodeUtil.asStringList(node);
return valueValidations.apply(values, ctx);
}
private ReportItems defaultValidation(List<String> nodeValues, RunContext ctx) {
return success(ctx);
}
}

View File

@ -3,62 +3,61 @@ package org.oneedtech.inspect.vc.probe;
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import java.util.List; 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.probe.RunContext;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Credential; import org.oneedtech.inspect.vc.Credential.CredentialEnum;
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;
/** /**
* A Probe that verifies a credential's type property. * A Probe that verifies a credential's type property.
* *
* @author mgylling * @author mgylling
*/ */
public class TypePropertyProbe extends Probe<JsonNode> { public class TypePropertyProbe extends StringValuePropertyProbe {
private final Credential.Type expected; private final CredentialEnum expected;
public TypePropertyProbe(Credential.Type expected) { public TypePropertyProbe(CredentialEnum expected) {
super(ID); super(ID, expected.toString(), "type");
this.expected = checkNotNull(expected); this.expected = checkNotNull(expected);
this.setValueValidations(this::validate);
} }
@Override public ReportItems validate(List<String> values, RunContext ctx) {
public ReportItems run(JsonNode root, RunContext ctx) throws Exception { List<String> requiredTypeValues = expected.getRequiredTypeValues();
if (!requiredTypeValues.isEmpty()) {
ArrayNode typeNode = (ArrayNode) root.get("type"); if (!requiredTypeValues.stream().allMatch(requiredValue -> values.contains(requiredValue))) {
if (typeNode == null) return fatal(formatMessage(requiredTypeValues), ctx);
return fatal("No type property", ctx); }
List<String> values = JsonNodeUtil.asStringList(typeNode);
if (!values.contains("VerifiableCredential")) {
return fatal("The type property does not contain the entry 'VerifiableCredential'", ctx);
} }
if (expected == Credential.Type.OpenBadgeCredential) { if (expected.isAllowedTypeValuesRequired()) {
if (!values.contains("OpenBadgeCredential") && !values.contains("AchievementCredential")) { List<String> allowedValues = expected.getAllowedTypeValues();
return fatal("The type property does not contain one of 'OpenBadgeCredential' or 'AchievementCredential'", ctx); if (allowedValues.isEmpty()) {
return fatal("The type property is invalid", ctx);
} }
} else if (expected == Credential.Type.ClrCredential) { if (!values.stream().anyMatch(v -> allowedValues.contains(v))) {
if (!values.contains("ClrCredential")) { return fatal(formatMessage(values), ctx);
return fatal("The type property does not contain the entry 'ClrCredential'", 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); return success(ctx);
} }
private String formatMessage(List<String> 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(); public static final String ID = TypePropertyProbe.class.getSimpleName();
} }

View File

@ -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<JsonLdGeneratedObject> {
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<String> 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<String> 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();
}

View File

@ -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<JsonLdGeneratedObject> {
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<String> 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<String> allowedTypes = List.of("id", "email", "url", "telephone");
public static final String ID = VerificationJWTProbe.class.getSimpleName();
}

View File

@ -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<Assertion> {
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<String> 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<String> allowedTypes = List.of("id", "email", "url", "telephone");
public static final String ID = VerificationRecipientProbe.class.getSimpleName();
}

View File

@ -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<MimeType> allowedMimeTypes = List.of(MimeType.IMAGE_PNG, MimeType.IMAGE_SVG);
public static final String ID = ValidationImagePropertyProbe.class.getSimpleName();
}

View File

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

View File

@ -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<String> values = JsonNodeUtil.asStringList(node);
if (values == null ||values.isEmpty()) {
return error("Required property " + validation.getName() + " value " + values + " is not acceptable", ctx);
}
}
}
List<JsonNode> 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<JsonNode, Boolean> 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<ReportItems> 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<ReportItems> 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();
}

View File

@ -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);
}
}

View File

@ -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<String> 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();
}

View File

@ -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));
}
}

View File

@ -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;
}

View File

@ -2,8 +2,12 @@ package org.oneedtech.inspect.vc.util;
import java.io.InputStream; import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.time.Duration; 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.LogManager;
import org.apache.logging.log4j.Logger; 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.collect.ImmutableMap;
import com.google.common.io.Resources; import com.google.common.io.Resources;
import foundation.identity.jsonld.ConfigurableDocumentLoader;
/** /**
* A com.apicatalog DocumentLoader with a threadsafe static cache. * A com.apicatalog DocumentLoader with a threadsafe static cache.
* *
* @author mgylling * @author mgylling
*/ */
public class CachingDocumentLoader implements DocumentLoader { public class CachingDocumentLoader extends ConfigurableDocumentLoader {
private Set<URI> contexts;
public CachingDocumentLoader() {
this(null);
}
public CachingDocumentLoader(Map<URI, String> localDomains) {
super();
setEnableHttp(true);
setEnableHttps(true);
setDefaultHttpLoader(new HttpLoader(localDomains));
this.contexts = new HashSet<>();
}
@Override @Override
public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError { public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError {
Tuple<String, DocumentLoaderOptions> tpl = new Tuple<>(url.toASCIIString(), options); Document document = super.loadDocument(url, options);
try { if (document == null) {
return documentCache.get(tpl);
} catch (Exception e) {
logger.error("documentCache not able to load {}", url); 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<URI> getContexts() {
return contexts;
}
public void resetContexts() {
this.contexts.clear();
}
public class HttpLoader implements DocumentLoader {
final Map<URI, String> localDomains;
public HttpLoader(Map<URI, String> localDomains) {
this.localDomains = localDomains;
}
@Override
public Document loadDocument(URI url, DocumentLoaderOptions options) throws JsonLdError {
try {
// resolve url
URI resolvedUrl = resolve(url);
Tuple<String, DocumentLoaderOptions> 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/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/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://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(); .build();
@ -62,7 +135,8 @@ public class CachingDocumentLoader implements DocumentLoader {
.initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24)) .initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24))
.build(new CacheLoader<Tuple<String, DocumentLoaderOptions>, Document>() { .build(new CacheLoader<Tuple<String, DocumentLoaderOptions>, Document>() {
public Document load(final Tuple<String, DocumentLoaderOptions> id) throws Exception { public Document load(final Tuple<String, DocumentLoaderOptions> 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() ? bundled.get(id.t1).openStream()
: new URI(id.t1).toURL().openStream();) { : new URI(id.t1).toURL().openStream();) {
return JsonDocument.of(is); return JsonDocument.of(is);

View File

@ -17,11 +17,11 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
public class JsonNodeUtil { public class JsonNodeUtil {
public static List<JsonNode> asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) { public static List<JsonNode> asNodeList(JsonNode root, String jsonPath, JsonPathEvaluator evaluator) {
List<JsonNode> list = new ArrayList<>(); List<JsonNode> list = new ArrayList<>();
ArrayNode array = evaluator.eval(jsonPath, root); ArrayNode array = evaluator.eval(jsonPath, root);
for(JsonNode node : array) { for(JsonNode node : array) {
if(!(node instanceof ArrayNode)) { if(!(node instanceof ArrayNode)) {
list.add(node); list.add(node);
} else { } else {
ArrayNode values = (ArrayNode) node; ArrayNode values = (ArrayNode) node;
for(JsonNode value : values) { for(JsonNode value : values) {
@ -31,28 +31,39 @@ public class JsonNodeUtil {
} }
return list; return list;
} }
public static List<String> asStringList(JsonNode node) { public static List<String> asStringList(JsonNode node) {
if(!(node instanceof ArrayNode)) { if(!(node instanceof ArrayNode)) {
if (node.isObject()) {
return List.of();
}
return List.of(node.asText()); return List.of(node.asText());
} else { } else {
ArrayNode arrayNode = (ArrayNode)node; ArrayNode arrayNode = (ArrayNode)node;
return StreamSupport return StreamSupport
.stream(arrayNode.spliterator(), false) .stream(arrayNode.spliterator(), false)
.map(n->n.asText().strip()) .map(n->n.asText().strip())
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
} }
public static List<JsonNode> asNodeList(JsonNode node) { public static List<JsonNode> asNodeList(JsonNode node) {
if(node == null) return null; if(node == null) return null;
if(!(node instanceof ArrayNode)) { if(!(node instanceof ArrayNode)) {
return List.of(node); return List.of(node);
} else { } else {
ArrayNode arrayNode = (ArrayNode)node; ArrayNode arrayNode = (ArrayNode)node;
return StreamSupport return StreamSupport
.stream(arrayNode.spliterator(), false) .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();
}
} }

View File

@ -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;
}
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
}

View File

@ -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<String> localDomains = List.of("https://www.example.org/", "https://example.org/", "http://example.org/");
}

View File

@ -3,41 +3,114 @@ package org.oneedtech.inspect.vc;
import org.oneedtech.inspect.test.Sample; import org.oneedtech.inspect.test.Sample;
public class Samples { public class Samples {
public static final class OB30 { public static final class OB30 {
public static final class SVG { public static final class SVG {
public final static Sample SIMPLE_JSON_SVG = new Sample("ob30/simple-json.svg", true); 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 COMPLETE_JSON = new Sample("ob30/complete.json", false);
public final static Sample SIMPLE_JSON = new Sample("ob30/simple.json", true); public final static Sample SIMPLE_JSON = 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_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_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_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_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_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_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_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_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_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_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_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 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_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 final static Sample SIMPLE_JWT = new Sample("ob30/simple.jwt", true);
} }
} }
public static final class CLR20 { public static final class CLR20 {
public static final class JSON { public static final class JSON {
public final static Sample SIMPLE_JSON = new Sample("clr20/simple.json", true); 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_JSON_NOPROOF = new Sample("clr20/simple-noproof.json", true);
public final static Sample SIMPLE_JWT = new Sample("clr20/simple.jwt", 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);
}
}
} }

View File

@ -1,10 +1,9 @@
package org.oneedtech.inspect.vc.credential; 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 static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.RunContext.Key; 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.Credential;
import org.oneedtech.inspect.vc.OB30Inspector; import org.oneedtech.inspect.vc.OB30Inspector;
import org.oneedtech.inspect.vc.Samples; 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.PayloadParser;
import org.oneedtech.inspect.vc.payload.PayloadParserFactory; 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; import com.fasterxml.jackson.databind.ObjectMapper;
public class PayloadParserTests { public class PayloadParserTests {
@Test @Test
void testSvgStringExtract() { void testSvgStringExtract() {
assertDoesNotThrow(()->{ assertDoesNotThrow(()->{
Resource res = Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource(ResourceType.SVG); Resource res = Samples.OB30.SVG.SIMPLE_JSON_SVG.asFileResource(ResourceType.SVG);
PayloadParser ext = PayloadParserFactory.of(res); PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext); assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd);
assertNotNull(crd.getJson());
assertNotNull(crd.getJson().get("@context"));
});
}
@Test
void testSvgJwtExtract() {
assertDoesNotThrow(()->{
Resource res = Samples.OB30.SVG.SIMPLE_JWT_SVG.asFileResource(ResourceType.SVG);
PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res)); Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString()); //System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd); 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() { void testPngStringExtract() {
assertDoesNotThrow(()->{ assertDoesNotThrow(()->{
Resource res = Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource(ResourceType.PNG); Resource res = Samples.OB30.PNG.SIMPLE_JSON_PNG.asFileResource(ResourceType.PNG);
PayloadParser ext = PayloadParserFactory.of(res); PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext); assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res)); Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString()); //System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd); assertNotNull(crd);
@ -63,13 +65,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context")); assertNotNull(crd.getJson().get("@context"));
}); });
} }
@Test @Test
void testPngJwtExtract() { void testPngJwtExtract() {
assertDoesNotThrow(()->{ assertDoesNotThrow(()->{
Resource res = Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource(ResourceType.PNG); Resource res = Samples.OB30.PNG.SIMPLE_JWT_PNG.asFileResource(ResourceType.PNG);
PayloadParser ext = PayloadParserFactory.of(res); PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext); assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res)); Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString()); //System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd); assertNotNull(crd);
@ -77,13 +79,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context")); assertNotNull(crd.getJson().get("@context"));
}); });
} }
@Test @Test
void testJwtExtract() { void testJwtExtract() {
assertDoesNotThrow(()->{ assertDoesNotThrow(()->{
Resource res = Samples.OB30.JWT.SIMPLE_JWT.asFileResource(ResourceType.JWT); Resource res = Samples.OB30.JWT.SIMPLE_JWT.asFileResource(ResourceType.JWT);
PayloadParser ext = PayloadParserFactory.of(res); PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext); assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res)); Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString()); //System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd); assertNotNull(crd);
@ -91,13 +93,13 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context")); assertNotNull(crd.getJson().get("@context"));
}); });
} }
@Test @Test
void testJsonExtract() { void testJsonExtract() {
assertDoesNotThrow(()->{ assertDoesNotThrow(()->{
Resource res = Samples.OB30.JSON.SIMPLE_JSON.asFileResource(ResourceType.JSON); Resource res = Samples.OB30.JSON.SIMPLE_JSON.asFileResource(ResourceType.JSON);
PayloadParser ext = PayloadParserFactory.of(res); PayloadParser ext = PayloadParserFactory.of(res);
assertNotNull(ext); assertNotNull(ext);
Credential crd = ext.parse(res, mockOB30Context(res)); Credential crd = ext.parse(res, mockOB30Context(res));
//System.out.println(crd.getJson().toPrettyString()); //System.out.println(crd.getJson().toPrettyString());
assertNotNull(crd); assertNotNull(crd);
@ -105,7 +107,7 @@ public class PayloadParserTests {
assertNotNull(crd.getJson().get("@context")); assertNotNull(crd.getJson().get("@context"));
}); });
} }
private RunContext mockOB30Context(Resource res) { private RunContext mockOB30Context(Resource res) {
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT); ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
@ -114,6 +116,10 @@ public class PayloadParserTests {
.put(res) .put(res)
.put(Key.JACKSON_OBJECTMAPPER, mapper) .put(Key.JACKSON_OBJECTMAPPER, mapper)
.put(Key.JSONPATH_EVALUATOR, jsonPath) .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(); .build();
} }
} }

View File

@ -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);
}
}

View File

@ -1,27 +1,47 @@
package org.oneedtech.inspect.vc.util; package org.oneedtech.inspect.vc.util;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.util.Map;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import com.apicatalog.jsonld.document.Document; import com.apicatalog.jsonld.document.Document;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.loader.DocumentLoader; import com.apicatalog.jsonld.loader.DocumentLoader;
import com.apicatalog.jsonld.loader.DocumentLoaderOptions; import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.google.common.io.Resources;
public class CachingDocumentLoaderTests { public class CachingDocumentLoaderTests {
@Test @Test
void testStaticCachedDocumentBundled() { void testStaticCachedDocumentBundled() {
Assertions.assertDoesNotThrow(()->{ Assertions.assertDoesNotThrow(()->{
DocumentLoader loader = new CachingDocumentLoader(); DocumentLoader loader = new CachingDocumentLoader();
for(String id : CachingDocumentLoader.bundled.keySet()) { for(String id : CachingDocumentLoader.bundled.keySet()) {
Document doc = loader.loadDocument(new URI(id), new DocumentLoaderOptions()); Document doc = loader.loadDocument(new URI(id), new DocumentLoaderOptions());
Assertions.assertNotNull(doc); Assertions.assertNotNull(doc);
} }
}); });
} }
@Test
void testLocalDomainCachedDocument() {
Assertions.assertDoesNotThrow(()->{
Map<URI, String> 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 @Test
void testStaticCachedDocumentKey() { void testStaticCachedDocumentKey() {
Assertions.assertDoesNotThrow(()->{ Assertions.assertDoesNotThrow(()->{
@ -30,5 +50,5 @@ public class CachingDocumentLoaderTests {
Document doc = loader.loadDocument(uri, new DocumentLoaderOptions()); Document doc = loader.loadDocument(uri, new DocumentLoaderOptions());
Assertions.assertNotNull(doc); Assertions.assertNotNull(doc);
}); });
} }
} }

View File

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> goodValues = List.of("google.com", "nerds.example.com");
List<String> 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<String> goodValues = List.of("id", "email", "telephone", "url");
List<String> badValues = List.of("sloths");
assertFunction(ValueType.COMPACT_IRI, goodValues, badValues);
}
@Test
void testBasicText() throws JsonMappingException, JsonProcessingException {
List<String> goodValues = List.of("string value");
List<Integer> badValues = List.of(3, 4);
assertFunction(ValueType.TEXT, goodValues, badValues);
}
@Test
void testTelephone() throws JsonMappingException, JsonProcessingException {
List<String> goodValues = List.of("+64010", "+15417522845", "+18006664358", "+18006662344;ext=666");
List<String> 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<String> goodValues = List.of("abc@localhost", "cool+uncool@example.org");
List<String> badValues = List.of(" spacey@gmail.com", "steveman [at] gee mail dot com");
assertFunction(ValueType.EMAIL, goodValues, badValues);
}
@Test
void testBoolean() throws JsonMappingException, JsonProcessingException {
List<Boolean> goodValues = List.of(true, false);
List<String> badValues = List.of(" spacey@gmail.com", "steveman [at] gee mail dot com");
assertFunction(ValueType.BOOLEAN, goodValues, badValues);
}
@Test
void testDateTime() throws JsonMappingException, JsonProcessingException {
List<String> 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<String> badValues = List.of("notadatetime", "1977-06-10T12:00:00");
assertFunction(ValueType.DATETIME, goodValues, badValues);
}
private void assertFunction(ValueType valueType, List<? extends Object> goodValues, List<? extends Object> badValues) throws JsonMappingException, JsonProcessingException {
Function<JsonNode, Boolean> 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());
}
}

View File

@ -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<URI, String> 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<URI, String> 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);
}
}
}

View File

@ -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"
}
}

View File

@ -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"]
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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"]
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -0,0 +1,3 @@
{
"narrative": "Do the important things."
}

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@ -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."
}

View File

@ -0,0 +1,6 @@
{
"@context": "https://w3id.org/openbadges/v2",
"id": "http://example.org/empty-revocation-list.json",
"type": "RevocationList",
"revokedAssertions": []
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"]
}
}

View File

@ -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"
}
}

View File

@ -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"]
}
}

View File

@ -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/"
}
}

View File

@ -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"]
}
}

View File

@ -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"
}

View File

@ -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-----"
}

View File

@ -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-----"
}

View File

@ -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-----"
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
]
}

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

View File

@ -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"
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UuanNvbiIsInJlY2lwaWVudCI6eyJ0eXBlIjoiZW1haWwiLCJoYXNoZWQiOnRydWUsInNhbHQiOiJkZWFkc2VhIiwiaWRlbnRpdHkiOiJzaGEyNTYkZWNmNTQwOWYzZjRiOTFhYjYwY2M1ZWY0YzAyYWVmNzAzMjM1NDM3NWU3MGNmNGQ4ZTQzZjZhMWQyOTg5MTk0MiJ9LCJpbWFnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3QtYmFkZ2UucG5nIiwiZXZpZGVuY2UiOiJodHRwczovL2V4YW1wbGUub3JnL2JldGhzLXJvYm90LXdvcmsuaHRtbCIsImlzc3VlZE9uIjoiMjAxNi0xMi0zMVQyMzo1OTo1OVoiLCJiYWRnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmFkZ2UtZnJvbS1vcmdhbml6YXRpb24td2l0aC1lbXB0eS1yZXZvY2F0aW9uLWxpc3QuanNvbiIsInZlcmlmaWNhdGlvbiI6eyJ0eXBlIjoic2lnbmVkIiwiY3JlYXRvciI6Imh0dHA6Ly9leGFtcGxlLm9yZy9rZXkyLmpzb24ifX0.gLjqNnlBSJ3oWt84iPhVaFjYGR2bhrSLDnTHh5mcjujO_VxYFop5EJlGHV4C7UC6xnM01Yy4y8JxqCIvLLudVRrPtK3pYumWJvGnc43cNyjZq8IY6L1qrsklP_gJyV0P9IKUzh6kQDdSGx_1TwYY3qwKlLXqNuxqkjdcD0z3zlhYfCnEpk7GZAXP_1Np123g-R9i3OZ18cxcUrCo1W7fZGkH4NXayXvdQpGwKmyCiju0N3ZLbHtfcbdWyVLLDF1RJJuRoVRgETly4uqVu8aFJ9O62HuOJggF7eLTos1gg-M2IpCa7yuUDVqGCWIRun5tbuUbJv_0Kg68lCslpZ09aw

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UtcmV2b2tlZC5qc29uIiwicmVjaXBpZW50Ijp7InR5cGUiOiJlbWFpbCIsImhhc2hlZCI6dHJ1ZSwic2FsdCI6ImRlYWRzZWEiLCJpZGVudGl0eSI6InNoYTI1NiRlY2Y1NDA5ZjNmNGI5MWFiNjBjYzVlZjRjMDJhZWY3MDMyMzU0Mzc1ZTcwY2Y0ZDhlNDNmNmExZDI5ODkxOTQyIn0sImltYWdlIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9iZXRocy1yb2JvdC1iYWRnZS5wbmciLCJldmlkZW5jZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3Qtd29yay5odG1sIiwiaXNzdWVkT24iOiIyMDE2LTEyLTMxVDIzOjU5OjU5WiIsImJhZGdlIjoiaHR0cHM6Ly9leGFtcGxlLm9yZy9iYWRnZS1mcm9tLW9yZ2FuaXphdGlvbi13aXRoLXJldm9jYXRpb24tbGlzdC5qc29uIiwidmVyaWZpY2F0aW9uIjp7InR5cGUiOiJzaWduZWQiLCJjcmVhdG9yIjoiaHR0cDovL2V4YW1wbGUub3JnL2tleTMuanNvbiJ9fQ.RPJ0gudgtSYwjlZLnMmPHuL_SZmlW46cD3GXE8JQgOk02RwofMUt1w14ey8x7nWexXhvoBIA63qT4S0MfVuMeznAzNMO9_Upm0fworkR500257402NoXTnyzvF8Z8j_bCfoDZrdvljHFCieE1txmhDvRLvR5_y8KaZ6XC3fggokqMUQj6Is6LO4rnazqQixZWT8h_mPbE18_lUi0ocvHdrkwVWIeQi8_B-vgsUOANoBkX9ALBwHnqgfmfOwd-zGV39rBY4lj2nAxoHmY4VClqRj3Qqzoml7bd2eIhFtkJUmxfFuKdcfVhknWqw72MDGb6T-iCKYjuBNhYdxw6i1yQg

View File

@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvb3BlbmJhZGdlcy92MiIsInR5cGUiOiJBc3NlcnRpb24iLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3RpY3MtYmFkZ2UuanNvbiIsInJlY2lwaWVudCI6eyJ0eXBlIjoiZW1haWwiLCJoYXNoZWQiOnRydWUsInNhbHQiOiJkZWFkc2VhIiwiaWRlbnRpdHkiOiJzaGEyNTYkZWNmNTQwOWYzZjRiOTFhYjYwY2M1ZWY0YzAyYWVmNzAzMjM1NDM3NWU3MGNmNGQ4ZTQzZjZhMWQyOTg5MTk0MiJ9LCJpbWFnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvYmV0aHMtcm9ib3QtYmFkZ2UucG5nIiwiZXZpZGVuY2UiOiJodHRwczovL2V4YW1wbGUub3JnL2JldGhzLXJvYm90LXdvcmsuaHRtbCIsImlzc3VlZE9uIjoiMjAxNi0xMi0zMVQyMzo1OTo1OVoiLCJiYWRnZSI6Imh0dHBzOi8vZXhhbXBsZS5vcmcvcm9ib3RpY3MtYmFkZ2UuanNvbiIsInZlcmlmaWNhdGlvbiI6eyJ0eXBlIjoic2lnbmVkIiwiY3JlYXRvciI6Imh0dHA6Ly9leGFtcGxlLm9yZy9rZXkxLmpzb24ifX0.bR5tt-GEneb7tdWfmDdCq2FCBRuvSnKxNIfMPVz4lXiLcC_MobAB1zC9VQTJ5wJMzyS7Z7SR6eqt5WjuV4gl-6cT1qm3Af4VaKMEpi5bc3w2EGMlw10YxYAIrFEmdAXod9jwA2hPd9k4ypuIIIyzThRAhxVY36vf6nZWkC11MNj0QJoAJ-q2JlaERaGnwSOf1h63jUIyWtsfs6ocXb_hiFIaxPKAqnJSnWeF672vo3lOG_0vM6Tk4Ug73DRzJOsJhGwSWWze_GmwgqtUcWS2b8HmpoEaS_1gXwqnhNxiNNN0a39P-MqP86cY6pb64WKcIVY0ZSnvb4mwzXzZ3cPOTg

View File

@ -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"
}
}

View File

@ -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"
}
}