Merge pull request #28 from imsglc/credential-type-mods
Credential type mods + Signature Verification + 50% of Proof
This commit is contained in:
		
						commit
						cb7a98f378
					
				@ -11,5 +11,30 @@
 | 
				
			|||||||
			<groupId>org.1edtech</groupId>
 | 
								<groupId>org.1edtech</groupId>
 | 
				
			||||||
			<artifactId>inspector-core</artifactId>			
 | 
								<artifactId>inspector-core</artifactId>			
 | 
				
			||||||
		</dependency>
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.bouncycastle</groupId>
 | 
				
			||||||
 | 
								<artifactId>bcprov-jdk15to18</artifactId>
 | 
				
			||||||
 | 
								<version>1.65</version>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>com.auth0</groupId>
 | 
				
			||||||
 | 
								<artifactId>auth0</artifactId>
 | 
				
			||||||
 | 
								<version>1.20.0</version>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>com.auth0</groupId>
 | 
				
			||||||
 | 
								<artifactId>jwks-rsa</artifactId>
 | 
				
			||||||
 | 
								<version>0.12.0</version>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>com.auth0</groupId>
 | 
				
			||||||
 | 
								<artifactId>java-jwt</artifactId>
 | 
				
			||||||
 | 
								<version>3.10.3</version>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.springframework</groupId>
 | 
				
			||||||
 | 
								<artifactId>spring-core</artifactId>
 | 
				
			||||||
 | 
								<version>5.0.12.RELEASE</version>
 | 
				
			||||||
 | 
							</dependency>		
 | 
				
			||||||
	</dependencies>
 | 
						</dependencies>
 | 
				
			||||||
</project>
 | 
					</project>
 | 
				
			||||||
@ -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() {
 | 
				
			||||||
@ -52,6 +53,10 @@ public class Credential extends GeneratedObject  {
 | 
				
			|||||||
		return credentialType;
 | 
							return credentialType;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getJwt() {
 | 
				
			||||||
 | 
							return jwt;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Get the canonical schema for this credential if such exists.
 | 
						 * Get the canonical schema for this credential if such exists.
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
 | 
				
			|||||||
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.EndorsementInspector.ENDORSEMENT_KEY;
 | 
					import static org.oneedtech.inspect.vc.EndorsementInspector.ENDORSEMENT_KEY;
 | 
				
			||||||
import static org.oneedtech.inspect.vc.util.JsonNodeUtil.getEndorsements;
 | 
					import static org.oneedtech.inspect.vc.util.JsonNodeUtil.getEndorsements;
 | 
				
			||||||
 | 
					import static com.google.common.base.Strings.isNullOrEmpty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.net.URI;
 | 
					import java.net.URI;
 | 
				
			||||||
import java.util.ArrayList;
 | 
					import java.util.ArrayList;
 | 
				
			||||||
@ -105,15 +106,20 @@ public class OB30Inspector extends VCInspector {
 | 
				
			|||||||
				probeCount++;
 | 
									probeCount++;
 | 
				
			||||||
				accumulator.add(new InlineJsonSchemaProbe().run(crd, ctx));
 | 
									accumulator.add(new InlineJsonSchemaProbe().run(crd, ctx));
 | 
				
			||||||
				
 | 
									
 | 
				
			||||||
				//verify signatures TODO @Miles
 | 
									//If this credential was originally contained in a JWT we must validate the jwt and external proof.
 | 
				
			||||||
				probeCount++;
 | 
									if(!isNullOrEmpty(crd.getJwt())){
 | 
				
			||||||
				accumulator.add(new SignatureVerifierProbe().run(crd, ctx));
 | 
										probeCount++;
 | 
				
			||||||
				if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
 | 
										accumulator.add(new SignatureVerifierProbe().run(crd, ctx));
 | 
				
			||||||
 | 
										if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				
 | 
									
 | 
				
			||||||
				//verify proofs TODO @Miles
 | 
									//verify proofs TODO @Miles
 | 
				
			||||||
				probeCount++;
 | 
									//If this credential was not contained in a jwt it must have an internal proof.
 | 
				
			||||||
				accumulator.add(new ProofVerifierProbe().run(crd, ctx));
 | 
									if(isNullOrEmpty(crd.getJwt())){
 | 
				
			||||||
				if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
 | 
										probeCount++;
 | 
				
			||||||
 | 
										accumulator.add(new ProofVerifierProbe().run(crd, ctx));
 | 
				
			||||||
 | 
										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++;
 | 
				
			||||||
@ -147,8 +153,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;
 | 
				
			||||||
@ -44,13 +50,13 @@ public class CredentialTypeProbe extends Probe<Resource> {
 | 
				
			|||||||
			if(type.isPresent()) {
 | 
								if(type.isPresent()) {
 | 
				
			||||||
				resource.setType(type.get());
 | 
									resource.setType(type.get());
 | 
				
			||||||
				if(type.get() == ResourceType.PNG) {
 | 
									if(type.get() == ResourceType.PNG) {
 | 
				
			||||||
					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 = fromSVG(resource, context);
 | 
				
			||||||
				} else if(type.get() == ResourceType.JSON) {
 | 
									} else if(type.get() == ResourceType.JSON) {
 | 
				
			||||||
					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 = fromJWT(resource, context);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} 
 | 
								} 
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
@ -71,12 +77,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 +117,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 +128,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,57 +141,89 @@ 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)));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		//TODO @Miles
 | 
							//Deserialize and fetch the 'vc' node from the object
 | 
				
			||||||
 | 
							JsonNode outerPayload = buildNodeFromString(jwtPayload, context);
 | 
				
			||||||
 | 
							JsonNode vcNode = outerPayload.get("vc");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return vcNode;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private String getOpenBadgeCredentialNodeText(Node node){
 | 
				
			||||||
 | 
					        NamedNodeMap attributes = node.getAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//If this node is labeled with the attribute keyword: 'openbadgecredential' it is the right one.
 | 
				
			||||||
 | 
							if(attributes.getNamedItem("keyword") != null && attributes.getNamedItem("keyword").getNodeValue().equals("openbadgecredential")){
 | 
				
			||||||
 | 
								Node textAttribute = attributes.getNamedItem("text");
 | 
				
			||||||
 | 
								if(textAttribute != null) { return textAttribute.getNodeValue(); }
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//iterate over all children depth first and search for the credential node.
 | 
				
			||||||
 | 
							Node child = node.getFirstChild();
 | 
				
			||||||
 | 
							while (child != null)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            String nodeValue = getOpenBadgeCredentialNodeText(child);
 | 
				
			||||||
 | 
								if(nodeValue != null) { return nodeValue; }
 | 
				
			||||||
 | 
					            child = child.getNextSibling();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Return null if we haven't found anything at this recursive depth.
 | 
				
			||||||
		return null;
 | 
							return null;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,15 @@
 | 
				
			|||||||
package org.oneedtech.inspect.vc.probe;
 | 
					package org.oneedtech.inspect.vc.probe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.security.KeyFactory;
 | 
				
			||||||
 | 
					import java.security.Security;
 | 
				
			||||||
 | 
					import java.security.Signature;
 | 
				
			||||||
 | 
					import java.security.spec.X509EncodedKeySpec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.bouncycastle.asn1.edec.EdECObjectIdentifiers;
 | 
				
			||||||
 | 
					import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
 | 
				
			||||||
 | 
					import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
 | 
				
			||||||
 | 
					import org.bouncycastle.jce.provider.BouncyCastleProvider;
 | 
				
			||||||
 | 
					import org.bouncycastle.util.encoders.Hex;
 | 
				
			||||||
import org.oneedtech.inspect.core.probe.Probe;
 | 
					import org.oneedtech.inspect.core.probe.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;
 | 
				
			||||||
@ -23,5 +33,22 @@ public class ProofVerifierProbe extends Probe<Credential> {
 | 
				
			|||||||
		return success(ctx);
 | 
							return success(ctx);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public boolean validate(String pubkey, String signature, String timestamp, String message) throws Exception {
 | 
				
			||||||
 | 
							//TODO: continue this implementation.
 | 
				
			||||||
 | 
							//Pulled in bouncy castle library and made sure this sample compiled only.
 | 
				
			||||||
 | 
							final var provider = new BouncyCastleProvider();
 | 
				
			||||||
 | 
							Security.addProvider(provider);
 | 
				
			||||||
 | 
							final var byteKey = Hex.decode(pubkey);
 | 
				
			||||||
 | 
							final var pki = new SubjectPublicKeyInfo(new AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519), byteKey);
 | 
				
			||||||
 | 
							final var pkSpec = new X509EncodedKeySpec(pki.getEncoded());
 | 
				
			||||||
 | 
							final var kf = KeyFactory.getInstance("ed25519", provider);
 | 
				
			||||||
 | 
							final var publicKey = kf.generatePublic(pkSpec);
 | 
				
			||||||
 | 
							final var signedData = Signature.getInstance("ed25519", provider);
 | 
				
			||||||
 | 
							signedData.initVerify(publicKey);
 | 
				
			||||||
 | 
							signedData.update(timestamp.getBytes());
 | 
				
			||||||
 | 
							signedData.update(message.getBytes());
 | 
				
			||||||
 | 
							return signedData.verify(Hex.decode(signature));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static final String ID = ProofVerifierProbe.class.getSimpleName();
 | 
						public static final String ID = ProofVerifierProbe.class.getSimpleName();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,34 @@
 | 
				
			|||||||
package org.oneedtech.inspect.vc.probe;
 | 
					package org.oneedtech.inspect.vc.probe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static com.google.common.base.Strings.isNullOrEmpty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.math.BigInteger;
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.security.KeyFactory;
 | 
				
			||||||
 | 
					import java.security.PublicKey;
 | 
				
			||||||
 | 
					import java.security.interfaces.RSAPublicKey;
 | 
				
			||||||
 | 
					import java.security.spec.RSAPublicKeySpec;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Base64;
 | 
				
			||||||
 | 
					import java.util.Base64.Decoder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 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.Credential;
 | 
				
			||||||
 | 
					import org.springframework.util.Base64Utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.auth0.jwt.JWT;
 | 
				
			||||||
 | 
					import com.auth0.jwt.JWTVerifier;
 | 
				
			||||||
 | 
					import com.auth0.jwt.algorithms.Algorithm;
 | 
				
			||||||
 | 
					import com.auth0.jwt.exceptions.AlgorithmMismatchException;
 | 
				
			||||||
 | 
					import com.auth0.jwt.exceptions.InvalidClaimException;
 | 
				
			||||||
 | 
					import com.auth0.jwt.exceptions.SignatureVerificationException;
 | 
				
			||||||
 | 
					import com.auth0.jwt.exceptions.TokenExpiredException;
 | 
				
			||||||
 | 
					import com.auth0.jwt.interfaces.DecodedJWT;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.JsonNode;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.ObjectMapper;
 | 
				
			||||||
 | 
					import com.google.common.base.Splitter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * A Probe that verifies credential signatures 
 | 
					 * A Probe that verifies credential signatures 
 | 
				
			||||||
@ -17,12 +42,75 @@ public class SignatureVerifierProbe extends Probe<Credential> {
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
	public ReportItems run(Credential crd, RunContext ctx) throws Exception {
 | 
						public ReportItems run(Credential crd, RunContext ctx) throws Exception {
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
		//TODO @Miles -- if sigs fail, report OutCome.Fatal
 | 
								verifySignature(crd);
 | 
				
			||||||
 | 
							} catch (Exception e) {
 | 
				
			||||||
 | 
								return fatal("Error verifying jwt signature: " + e.getMessage(), ctx);
 | 
				
			||||||
 | 
							}	
 | 
				
			||||||
					
 | 
										
 | 
				
			||||||
		return success(ctx);
 | 
							return success(ctx);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void verifySignature(Credential crd) throws Exception {
 | 
				
			||||||
 | 
					        DecodedJWT decodedJwt = null;
 | 
				
			||||||
 | 
							String jwt = crd.getJwt();
 | 
				
			||||||
 | 
							if(isNullOrEmpty(jwt)) throw new IllegalArgumentException("invalid jwt");
 | 
				
			||||||
 | 
							List<String> parts = Splitter.on('.').splitToList(jwt);
 | 
				
			||||||
 | 
							if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							final Decoder decoder = Base64.getUrlDecoder();
 | 
				
			||||||
 | 
							String joseHeader = new String(decoder.decode(parts.get(0)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ObjectMapper mapper = new ObjectMapper();
 | 
				
			||||||
 | 
					    	JsonNode headerObj = mapper.readTree(joseHeader);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//MUST be "RS256"
 | 
				
			||||||
 | 
							JsonNode alg = headerObj.get("alg");
 | 
				
			||||||
 | 
							if(alg == null || !alg.textValue().equals("RS256")) { throw new Exception("alg must be present and must be 'RS256'"); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//TODO: decoded jwt will check timestamps, but shall we explicitly break these out?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Option 1, fetch directly from header
 | 
				
			||||||
 | 
							JsonNode jwk = headerObj.get("jwk");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Option 2, fetch from a hosting url
 | 
				
			||||||
 | 
							JsonNode kid = headerObj.get("kid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if(jwk == null && kid == null) { throw new Exception("Key must present in either jwk or kid value."); }
 | 
				
			||||||
 | 
							if(kid != null){
 | 
				
			||||||
 | 
								//TODO @Miles load jwk JsonNode from url and do the rest the same below.  Need to set up testing.
 | 
				
			||||||
 | 
								String kidUrl = kid.textValue();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							//Clean up may be required.  Currently need to cleanse extra double quoting.
 | 
				
			||||||
 | 
							String modulusString = jwk.get("n").textValue();
 | 
				
			||||||
 | 
							String exponentString = jwk.get("e").textValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							BigInteger modulus = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(modulusString));
 | 
				
			||||||
 | 
							BigInteger exponent = new BigInteger(1, org.springframework.util.Base64Utils.decodeFromUrlSafeString(exponentString));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							PublicKey pub = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, exponent));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Algorithm algorithm = Algorithm.RSA256((RSAPublicKey)pub, null);
 | 
				
			||||||
 | 
							JWTVerifier verifier = JWT.require(algorithm)
 | 
				
			||||||
 | 
									.build(); //Reusable verifier instance
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								decodedJwt = verifier.verify(jwt);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							catch(SignatureVerificationException ex){
 | 
				
			||||||
 | 
								throw new Exception("JWT Invalid signature", ex);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							catch(AlgorithmMismatchException ex){
 | 
				
			||||||
 | 
								throw new Exception("JWT Algorithm mismatch", ex);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							catch(TokenExpiredException ex){
 | 
				
			||||||
 | 
								throw new Exception("JWT Token expired", ex);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							catch(InvalidClaimException ex){
 | 
				
			||||||
 | 
								throw new Exception("JWT, one or more claims are invalid", ex);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	public static final String ID = SignatureVerifierProbe.class.getSimpleName();
 | 
						public static final String ID = SignatureVerifierProbe.class.getSimpleName();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -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  | 
							
								
								
									
										36
									
								
								inspector-vc/src/test/resources/ob30/simple-old-delete.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								inspector-vc/src/test/resources/ob30/simple-old-delete.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "@context": [
 | 
				
			||||||
 | 
					      "https://www.w3.org/2018/credentials/v1",
 | 
				
			||||||
 | 
					      "https://imsglobal.github.io/openbadges-specification/context.json",
 | 
				
			||||||
 | 
					      "https://w3id.org/security/suites/ed25519-2020/v1"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "id": "http://example.edu/credentials/3732",
 | 
				
			||||||
 | 
					    "type": [
 | 
				
			||||||
 | 
					      "VerifiableCredential",
 | 
				
			||||||
 | 
					      "OpenBadgeCredential"
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    "issuer": {
 | 
				
			||||||
 | 
					      "id": "https://example.edu/issuers/565049",
 | 
				
			||||||
 | 
					      "type": [
 | 
				
			||||||
 | 
					        "Profile"
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					      "name": "Example University"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "issuanceDate": "2010-01-01T00:00:00Z",
 | 
				
			||||||
 | 
					    "name": "Example University Degree",
 | 
				
			||||||
 | 
					    "credentialSubject": {
 | 
				
			||||||
 | 
					      "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
 | 
				
			||||||
 | 
					      "type": [
 | 
				
			||||||
 | 
					        "AchievementSubject"
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "proof": [
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        "type": "Ed25519Signature2020",
 | 
				
			||||||
 | 
					        "created": "2022-06-09T22:56:28Z",
 | 
				
			||||||
 | 
					        "verificationMethod": "https://example.edu/issuers/565049#key-1",
 | 
				
			||||||
 | 
					        "proofPurpose": "assertionMethod",
 | 
				
			||||||
 | 
					        "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
@ -1,36 +1,36 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "@context": [
 | 
					  "@context": [
 | 
				
			||||||
      "https://www.w3.org/2018/credentials/v1",
 | 
					    "https://www.w3.org/2018/credentials/v1",
 | 
				
			||||||
      "https://imsglobal.github.io/openbadges-specification/context.json",
 | 
					    "https://imsglobal.github.io/openbadges-specification/context.json",
 | 
				
			||||||
      "https://w3id.org/security/suites/ed25519-2020/v1"
 | 
					    "https://w3id.org/security/suites/ed25519-2020/v1"
 | 
				
			||||||
    ],
 | 
					  ],
 | 
				
			||||||
    "id": "http://example.edu/credentials/3732",
 | 
					  "id": "http://example.edu/credentials/3732",
 | 
				
			||||||
 | 
					  "type": [
 | 
				
			||||||
 | 
					    "VerifiableCredential",
 | 
				
			||||||
 | 
					    "OpenBadgeCredential"
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  "issuer": {
 | 
				
			||||||
 | 
					    "id": "https://example.edu/issuers/565049",
 | 
				
			||||||
    "type": [
 | 
					    "type": [
 | 
				
			||||||
      "VerifiableCredential",
 | 
					      "Profile"
 | 
				
			||||||
      "OpenBadgeCredential"
 | 
					 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "issuer": {
 | 
					    "name": "Example University"
 | 
				
			||||||
      "id": "https://example.edu/issuers/565049",
 | 
					  },
 | 
				
			||||||
      "type": [
 | 
					  "issuanceDate": "2010-01-01T00:00:00Z",
 | 
				
			||||||
        "Profile"
 | 
					  "name": "Example University Degree",
 | 
				
			||||||
      ],
 | 
					  "credentialSubject": {
 | 
				
			||||||
      "name": "Example University"
 | 
					    "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
 | 
				
			||||||
    },
 | 
					    "type": [
 | 
				
			||||||
    "issuanceDate": "2010-01-01T00:00:00Z",
 | 
					      "AchievementSubject"
 | 
				
			||||||
    "name": "Example University Degree",
 | 
					 | 
				
			||||||
    "credentialSubject": {
 | 
					 | 
				
			||||||
      "id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
 | 
					 | 
				
			||||||
      "type": [
 | 
					 | 
				
			||||||
        "AchievementSubject"
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "proof": [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        "type": "Ed25519Signature2020",
 | 
					 | 
				
			||||||
        "created": "2022-06-09T22:56:28Z",
 | 
					 | 
				
			||||||
        "verificationMethod": "https://example.edu/issuers/565049#key-1",
 | 
					 | 
				
			||||||
        "proofPurpose": "assertionMethod",
 | 
					 | 
				
			||||||
        "proofValue": "z58ieJCh4kN6eE2Vq4TyYURKSC4hWWEK7b75NNUL2taZMhKqwTteuByG1wRoGDdCqqNLW5Gq1diUi4qyZ63tQRtyN"
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  }
 | 
					  },
 | 
				
			||||||
 | 
					  "proof": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "type": "Ed25519Signature2020",
 | 
				
			||||||
 | 
					      "created": "2022-06-28T16:28:36Z",
 | 
				
			||||||
 | 
					      "verificationMethod": "did:key:z6MkkUD3J14nkYzn46QeuaVSnp7dF85QJKwKvJvfsjx79aXj",
 | 
				
			||||||
 | 
					      "proofPurpose": "assertionMethod",
 | 
				
			||||||
 | 
					      "proofValue": "z3MUt2ZuU8Byqivxh6GphEM65AFYyNaGYibm97xLTafM7uGufZQLKvJR8itZwxKskvtFM3CUty46v26DZidMNoQnM"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user