Rebaked a couple of badges. Added decode jwt code and png type probe. Refactored accordingly to retain external jwt.
Endorsements needs a refactor pending finality on endorsement lookup.
This commit is contained in:
parent
3ff7a0f76a
commit
a913ac97d8
@ -25,8 +25,9 @@ public class Credential extends GeneratedObject {
|
|||||||
final Resource resource;
|
final Resource resource;
|
||||||
final JsonNode jsonData;
|
final JsonNode jsonData;
|
||||||
final Credential.Type credentialType;
|
final Credential.Type credentialType;
|
||||||
|
final String jwt;
|
||||||
|
|
||||||
public Credential(Resource resource, JsonNode data) {
|
public Credential(Resource resource, JsonNode data, String jwt) {
|
||||||
super(ID, GeneratedObject.Type.INTERNAL);
|
super(ID, GeneratedObject.Type.INTERNAL);
|
||||||
checkNotNull(resource, resource.getType(), data);
|
checkNotNull(resource, resource.getType(), data);
|
||||||
ResourceType type = resource.getType();
|
ResourceType type = resource.getType();
|
||||||
@ -34,10 +35,10 @@ public class Credential extends GeneratedObject {
|
|||||||
"Unrecognized payload type: " + type.getName());
|
"Unrecognized payload type: " + type.getName());
|
||||||
this.resource = resource;
|
this.resource = resource;
|
||||||
this.jsonData = data;
|
this.jsonData = data;
|
||||||
|
this.jwt = jwt;
|
||||||
|
|
||||||
ArrayNode typeNode = (ArrayNode)jsonData.get("type");
|
ArrayNode typeNode = (ArrayNode)jsonData.get("type");
|
||||||
this.credentialType = Credential.Type.valueOf(typeNode);
|
this.credentialType = Credential.Type.valueOf(typeNode);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getResource() {
|
public Resource getResource() {
|
||||||
|
@ -147,8 +147,11 @@ public class OB30Inspector extends VCInspector {
|
|||||||
EndorsementInspector subInspector = new EndorsementInspector.Builder().build();
|
EndorsementInspector subInspector = new EndorsementInspector.Builder().build();
|
||||||
for(JsonNode endorsementNode : endorsements) {
|
for(JsonNode endorsementNode : endorsements) {
|
||||||
probeCount++;
|
probeCount++;
|
||||||
Credential endorsement = new Credential(resource, endorsementNode);
|
//TODO: @Markus @Miles, need to refactor to detect as this can be an internal or external proof credential.
|
||||||
accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement)));
|
//This will LIKELY come from two distinct sources in which case we would detect the type by property name.
|
||||||
|
//Third param to constructor: Compact JWT -> add third param after decoding. Internal Proof, null jwt string.
|
||||||
|
//Credential endorsement = new Credential(resource, endorsementNode);
|
||||||
|
//accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,16 @@ package org.oneedtech.inspect.vc.probe;
|
|||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Base64.Decoder;
|
import java.util.Base64.Decoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.ImageReader;
|
||||||
|
import javax.imageio.metadata.IIOMetadata;
|
||||||
import javax.xml.namespace.QName;
|
import javax.xml.namespace.QName;
|
||||||
import javax.xml.stream.XMLEventReader;
|
import javax.xml.stream.XMLEventReader;
|
||||||
import javax.xml.stream.events.Attribute;
|
import javax.xml.stream.events.Attribute;
|
||||||
@ -22,6 +26,8 @@ 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.util.xml.XMLInputFactoryCache;
|
import org.oneedtech.inspect.util.xml.XMLInputFactoryCache;
|
||||||
import org.oneedtech.inspect.vc.Credential;
|
import org.oneedtech.inspect.vc.Credential;
|
||||||
|
import org.w3c.dom.NamedNodeMap;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
@ -43,14 +49,19 @@ public class CredentialTypeProbe extends Probe<Resource> {
|
|||||||
|
|
||||||
if(type.isPresent()) {
|
if(type.isPresent()) {
|
||||||
resource.setType(type.get());
|
resource.setType(type.get());
|
||||||
|
//TODO: Refactor to return the entire credential so we can include optional encoded JWT.
|
||||||
if(type.get() == ResourceType.PNG) {
|
if(type.get() == ResourceType.PNG) {
|
||||||
crd = new Credential(resource, fromPNG(resource, context));
|
//crd = new Credential(resource, fromPNG(resource, context));
|
||||||
|
crd = fromPNG(resource, context);
|
||||||
} else if(type.get() == ResourceType.SVG) {
|
} else if(type.get() == ResourceType.SVG) {
|
||||||
crd = new Credential(resource, fromSVG(resource, context));
|
//crd = new Credential(resource, fromSVG(resource, context));
|
||||||
|
crd = fromSVG(resource, context);
|
||||||
} else if(type.get() == ResourceType.JSON) {
|
} else if(type.get() == ResourceType.JSON) {
|
||||||
crd = new Credential(resource, fromJson(resource, context));
|
//crd = new Credential(resource, fromJson(resource, context));
|
||||||
|
crd = fromJson(resource, context);
|
||||||
} else if(type.get() == ResourceType.JWT) {
|
} else if(type.get() == ResourceType.JWT) {
|
||||||
crd = new Credential(resource, fromJWT(resource, context));
|
//crd = new Credential(resource, fromJWT(resource, context));
|
||||||
|
crd = fromJWT(resource, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +82,39 @@ public class CredentialTypeProbe extends Probe<Resource> {
|
|||||||
* @param context
|
* @param context
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private JsonNode fromPNG(Resource resource, RunContext context) throws Exception {
|
private Credential fromPNG(Resource resource, RunContext context) throws Exception {
|
||||||
//TODO @Miles - note: iTxt chunk is either plain json or jwt
|
|
||||||
try(InputStream is = resource.asByteSource().openStream()) {
|
try(InputStream is = resource.asByteSource().openStream()) {
|
||||||
|
ImageReader imageReader = ImageIO.getImageReadersByFormatName("png").next();
|
||||||
|
imageReader.setInput(ImageIO.createImageInputStream(is), true);
|
||||||
|
IIOMetadata metadata = imageReader.getImageMetadata(0);
|
||||||
|
|
||||||
|
String credentialString = null;
|
||||||
|
String jwtString = null;
|
||||||
|
String formatSearch = null;
|
||||||
|
JsonNode credential = null;
|
||||||
|
String[] names = metadata.getMetadataFormatNames();
|
||||||
|
int length = names.length;
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
//Check all names rather than limiting to PNG format to remain malleable through any library changes. (Could limit to "javax_imageio_png_1.0")
|
||||||
|
formatSearch = getOpenBadgeCredentialNodeText(metadata.getAsTree(names[i]));
|
||||||
|
if(formatSearch != null) { credentialString = formatSearch; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if(credentialString == null) { throw new IllegalArgumentException("No credential inside PNG"); }
|
||||||
|
|
||||||
|
credentialString = credentialString.trim();
|
||||||
|
if(credentialString.charAt(0) != '{'){
|
||||||
|
//This is a jwt. Fetch either the 'vc' out of the payload and save the string for signature verification.
|
||||||
|
jwtString = credentialString;
|
||||||
|
credential = decodeJWT(credentialString,context);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
credential = buildNodeFromString(credentialString, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Credential(resource, credential, jwtString);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,8 +122,10 @@ public class CredentialTypeProbe extends Probe<Resource> {
|
|||||||
* @param context
|
* @param context
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
private JsonNode fromSVG(Resource resource, RunContext context) throws Exception {
|
private Credential fromSVG(Resource resource, RunContext context) throws Exception {
|
||||||
String json = null;
|
String json = null;
|
||||||
|
String jwtString = null;
|
||||||
|
JsonNode credential = null;;
|
||||||
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()) {
|
||||||
@ -93,7 +133,8 @@ public class CredentialTypeProbe extends Probe<Resource> {
|
|||||||
if(ev.isStartElement() && ev.asStartElement().getName().equals(OB_CRED_ELEM)) {
|
if(ev.isStartElement() && ev.asStartElement().getName().equals(OB_CRED_ELEM)) {
|
||||||
Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR);
|
Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR);
|
||||||
if(verifyAttr != null) {
|
if(verifyAttr != null) {
|
||||||
json = decodeJWT(verifyAttr.getValue());
|
jwtString = verifyAttr.getValue();
|
||||||
|
credential = decodeJWT(verifyAttr.getValue(), context);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
while(reader.hasNext()) {
|
while(reader.hasNext()) {
|
||||||
@ -105,58 +146,90 @@ public class CredentialTypeProbe extends Probe<Resource> {
|
|||||||
Characters chars = ev.asCharacters();
|
Characters chars = ev.asCharacters();
|
||||||
if(!chars.isWhiteSpace()) {
|
if(!chars.isWhiteSpace()) {
|
||||||
json = chars.getData();
|
json = chars.getData();
|
||||||
|
credential = buildNodeFromString(json, context);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(json!=null) break;
|
if(credential!=null) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(json == null) throw new IllegalArgumentException("No credential inside SVG");
|
if(credential == null) throw new IllegalArgumentException("No credential inside SVG");
|
||||||
return fromString(json, context);
|
return new Credential(resource, credential, jwtString);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JsonNode object from a raw JSON resource.
|
* Create a Credential object from a raw JSON resource.
|
||||||
* @param context
|
* @param context
|
||||||
*/
|
*/
|
||||||
private JsonNode fromJson(Resource resource, RunContext context) throws Exception {
|
private Credential fromJson(Resource resource, RunContext context) throws Exception {
|
||||||
return fromString(resource.asByteSource().asCharSource(UTF_8).read(), context);
|
return new Credential(resource, buildNodeFromString(resource.asByteSource().asCharSource(UTF_8).read(), context), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Credential object from a JWT resource.
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
private Credential fromJWT(Resource resource, RunContext context) throws Exception {
|
||||||
|
return new Credential(
|
||||||
|
resource,
|
||||||
|
decodeJWT(
|
||||||
|
resource.asByteSource().asCharSource(UTF_8).read(),
|
||||||
|
context
|
||||||
|
)
|
||||||
|
, resource.asByteSource().asCharSource(UTF_8).read()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JsonNode object from a String.
|
* Create a JsonNode object from a String.
|
||||||
*/
|
*/
|
||||||
private JsonNode fromString(String json, RunContext context) throws Exception {
|
private JsonNode buildNodeFromString(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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a JsonNode object from a JWT resource.
|
|
||||||
* @param context
|
|
||||||
*/
|
|
||||||
private JsonNode fromJWT(Resource resource, RunContext context) throws Exception {
|
|
||||||
return fromString(decodeJWT(resource.asByteSource().asCharSource(UTF_8).read()), context);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
private String decodeJWT(String jwt) {
|
private JsonNode decodeJWT(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();
|
||||||
String joseHeader = new String(decoder.decode(parts.get(0)));
|
//For this step we are only deserializing the stored badge out of the payload. The entire jwt is stored separately for
|
||||||
|
//signature verification later.
|
||||||
String jwtPayload = new String(decoder.decode(parts.get(1)));
|
String jwtPayload = new String(decoder.decode(parts.get(1)));
|
||||||
String jwsSignature = new String(decoder.decode(parts.get(2)));
|
|
||||||
|
//Deserialize and fetch the 'vc' node from the object
|
||||||
//TODO @Miles
|
JsonNode outerPayload = buildNodeFromString(jwtPayload, context);
|
||||||
|
JsonNode vcNode = outerPayload.get("vc");
|
||||||
return null;
|
|
||||||
|
return vcNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getOpenBadgeCredentialNodeText(Node node){
|
||||||
|
NamedNodeMap attributes = node.getAttributes();
|
||||||
|
|
||||||
|
//If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one.
|
||||||
|
if(attributes.getNamedItem("keyword") != null && attributes.getNamedItem("keyword").getNodeValue().equals("openbadgecredential")){
|
||||||
|
Node textAttribute = attributes.getNamedItem("text");
|
||||||
|
if(textAttribute != null) { return textAttribute.getNodeValue(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
//iterate over all children depth first and search for the credential node.
|
||||||
|
Node child = node.getFirstChild();
|
||||||
|
while (child != null)
|
||||||
|
{
|
||||||
|
String nodeValue = getOpenBadgeCredentialNodeText(child);
|
||||||
|
if(nodeValue != null) { return nodeValue; }
|
||||||
|
child = child.getNextSibling();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Return null if we haven't found anything at this recursive depth.
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential");
|
private static final QName OB_CRED_ELEM = new QName("https://purl.imsglobal.org/ob/v3p0", "credential");
|
||||||
|
@ -32,7 +32,6 @@ public class OB30Tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled
|
|
||||||
@Test
|
@Test
|
||||||
void testSimplePNGPlainValid() {
|
void testSimplePNGPlainValid() {
|
||||||
assertDoesNotThrow(()->{
|
assertDoesNotThrow(()->{
|
||||||
@ -42,7 +41,6 @@ public class OB30Tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled
|
|
||||||
@Test
|
@Test
|
||||||
void testSimplePNGJWTValid() {
|
void testSimplePNGJWTValid() {
|
||||||
assertDoesNotThrow(()->{
|
assertDoesNotThrow(()->{
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:openbadges="https://purl.imsglobal.org/ob/v3p0" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:openbadges="https://purl.imsglobal.org/ob/v3p0" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
|
||||||
<openbadges:credential verify="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaW1zZ2xvYmFsLmdpdGh1Yi5pby9vcGVuYmFkZ2VzLXNwZWNpZmljYXRpb24vY29udGV4dC5qc29uIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJFeGFtcGxlIFVuaXZlcnNpdHkifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IERlZ3JlZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.G7W8od9rSZRsVyk26rXjg_fH2CyUihwNpepd6tWgLt_UHC1vUU0Clox8IicnOSkMyYEqAuNZAdCC9_35i1oUcyj1c076Aa0dsVQ2fFVuQPqXBlyZWcBmo5jqOK6R9NHzRAYXwLRXgrB8gz3lSK55cnHTnMtkpXXcUcHkS5ylWbXCLeOWKoygOCuxRN3N6kP-0HOyuk15PWlnkJ2zEKz2pBtVPaNEydcT0kEtoHFMEWVwqo6rnGV-Ea3M7ssDt3145mcl-DVYLXmBVdT8KoO47QAOBaVMR6k-hgrHNBcdhpI-o6IvLIFsGLgrNvWN67i8Z7Baum1mP-HBpsAigdmIpA"></openbadges:credential>
|
<openbadges:credential verify="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImp3ayI6eyJlIjoiQVFBQiIsImt0eSI6IlJTQSIsIm4iOiJpMTljMlRlSWp5SjJCQWZ6cTdmYkdmMEl1RXJ6eGhzN0dnV0J2djZ6LXpqbDhCSjFDdDA1bmR4UzU0T2FHemZzY1JnckgxZnZqdnFZUi1tVlhFQXFRaFU2UG9LWlBuMGJoRUQ4Uk1lZlM0YTZfN2JnZVdpdTd3bmp0TzFlN2VYLXRxNUFwQzc4eGdGUW9Eemh1UXRqbDVHMjBJalRoR29sWGlwTEdLMmtoTjEyLUoxYVhCSUNaYVo5Q09UTk5zZDNSY1RXdGhBdDN6dDJLX09sSzUwV0VIYkV5Vk5qdlczcVZYQ1R5V0NXd1Y1WmN3dDFMX3AtV0VGU2lSRVgwb0lrNDZTYVo0bDk0c3N3WXk0STVveWtneVhpNUxYbEZjTVE0OVROZkpvUDYwRV92NDBGWnN2VTBjTHFmSkhOUThab0lHV2ZpcWRac2NkQ08ycXJLTUVybVEifX0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vaW1zZ2xvYmFsLmdpdGh1Yi5pby9vcGVuYmFkZ2VzLXNwZWNpZmljYXRpb24vY29udGV4dC5qc29uIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwidHlwZSI6WyJQcm9maWxlIl0sIm5hbWUiOiJFeGFtcGxlIFVuaXZlcnNpdHkifSwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IERlZ3JlZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXX19LCJpc3MiOiJodHRwczovL2V4YW1wbGUuZWR1L2lzc3VlcnMvNTY1MDQ5IiwibmJmIjoxMjYyMzA0MDAwLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.UGc9Ojaw9ivNBU6qOvn_V_yWeEEI1sUu3MBnULr0eVP0rqvslRzecdjKWy5ZcDv0SPk9ojGjzty7P1OKBRbBHAqH6Qh_vfRdjz3mXbVjwYU0tHPy7Tnqch3fQhZTCeJ6pEpfctRK6X4wwQrFEeAIAIuB-qOl7HVaWvzQeso4yYkg4sA7c9Xp0-1g2CzdL_VTQ8YoUp5KEn-cFAL3OvdQWl5flgBNOMsyxlhpqZ37BksSMFSUcoYDqTei8C6QG1124Hr2hcAtWMVq6zbWhhr23Gix6bkD8l1TMMQRKF1X1fIRlsdxQRlNWjBgTSKpM2uSmoL5PezDslF4K8r5_JD-7A"></openbadges:credential>
|
||||||
<g>
|
<g>
|
||||||
<path fill="none" stroke="#040000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M500,928.8c0,0,0,40.8,245,40.8" />
|
<path fill="none" stroke="#040000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M500,928.8c0,0,0,40.8,245,40.8" />
|
||||||
<path fill="none" stroke="#040000" stroke-width="1.9215" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M500,928.8c0,0,40.8,0,285.8,0" />
|
<path fill="none" stroke="#040000" stroke-width="1.9215" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M500,928.8c0,0,40.8,0,285.8,0" />
|
||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB |
Loading…
Reference in New Issue
Block a user