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 JsonNode jsonData; | ||||
| 	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); | ||||
| 		checkNotNull(resource, resource.getType(), data); | ||||
| 		ResourceType type = resource.getType(); | ||||
| @ -34,10 +35,10 @@ public class Credential extends GeneratedObject  { | ||||
| 				"Unrecognized payload type: " + type.getName()); | ||||
| 		this.resource = resource; | ||||
| 		this.jsonData = data; | ||||
| 		this.jwt = jwt; | ||||
| 		 | ||||
| 		ArrayNode typeNode = (ArrayNode)jsonData.get("type");		 | ||||
| 		this.credentialType = Credential.Type.valueOf(typeNode); | ||||
| 		 | ||||
| 	} | ||||
| 		 | ||||
| 	public Resource getResource() { | ||||
|  | ||||
| @ -147,8 +147,11 @@ public class OB30Inspector extends VCInspector { | ||||
| 					EndorsementInspector subInspector = new EndorsementInspector.Builder().build();	 | ||||
| 					for(JsonNode endorsementNode : endorsements) { | ||||
| 						probeCount++; | ||||
| 						Credential endorsement = new Credential(resource, endorsementNode); | ||||
| 						accumulator.add(subInspector.run(resource, Map.of(ENDORSEMENT_KEY, endorsement))); | ||||
| 						//TODO: @Markus @Miles, need to refactor to detect as this can be an internal or external proof credential. | ||||
| 						//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 java.io.ByteArrayInputStream; | ||||
| import java.io.InputStream; | ||||
| import java.util.Base64; | ||||
| import java.util.Base64.Decoder; | ||||
| import java.util.List; | ||||
| 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.stream.XMLEventReader; | ||||
| 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.xml.XMLInputFactoryCache; | ||||
| 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.ObjectMapper; | ||||
| @ -43,14 +49,19 @@ public class CredentialTypeProbe extends Probe<Resource> { | ||||
| 			 | ||||
| 			if(type.isPresent()) { | ||||
| 				resource.setType(type.get()); | ||||
| 				//TODO: Refactor to return the entire credential so we can include optional encoded JWT. | ||||
| 				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) { | ||||
| 					crd = new Credential(resource, fromSVG(resource, context)); | ||||
| 					//crd = new Credential(resource, fromSVG(resource, context)); | ||||
| 					crd = fromSVG(resource, context); | ||||
| 				} 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) { | ||||
| 					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  | ||||
| 	 * @throws Exception  | ||||
| 	 */ | ||||
| 	private JsonNode fromPNG(Resource resource, RunContext context) throws Exception { | ||||
| 		//TODO @Miles - note: iTxt chunk is either plain json or jwt	 | ||||
| 	private Credential fromPNG(Resource resource, RunContext context) throws Exception {	 | ||||
| 		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  | ||||
| 	 * @throws Exception  | ||||
| 	 */ | ||||
| 	private JsonNode fromSVG(Resource resource, RunContext context) throws Exception { | ||||
| 	private Credential fromSVG(Resource resource, RunContext context) throws Exception { | ||||
| 		String json = null; | ||||
| 		String jwtString = null; | ||||
| 		JsonNode credential = null;; | ||||
| 		try(InputStream is = resource.asByteSource().openStream()) { | ||||
| 			XMLEventReader reader = XMLInputFactoryCache.getInstance().createXMLEventReader(is); | ||||
| 			while(reader.hasNext()) { | ||||
| @ -93,7 +133,8 @@ public class CredentialTypeProbe extends Probe<Resource> { | ||||
| 				if(ev.isStartElement() && ev.asStartElement().getName().equals(OB_CRED_ELEM)) { | ||||
| 					Attribute verifyAttr = ev.asStartElement().getAttributeByName(OB_CRED_VERIFY_ATTR); | ||||
| 					if(verifyAttr != null) { | ||||
| 						json = decodeJWT(verifyAttr.getValue()); | ||||
| 						jwtString = verifyAttr.getValue(); | ||||
| 						credential = decodeJWT(verifyAttr.getValue(), context); | ||||
| 						break; | ||||
| 					} else { | ||||
| 						while(reader.hasNext()) { | ||||
| @ -105,58 +146,90 @@ public class CredentialTypeProbe extends Probe<Resource> { | ||||
| 								Characters chars = ev.asCharacters(); | ||||
| 								if(!chars.isWhiteSpace()) { | ||||
| 									json = chars.getData(); | ||||
| 									credential = buildNodeFromString(json, context); | ||||
| 									break; | ||||
| 								} | ||||
| 							} | ||||
| 						}						 | ||||
| 					}					 | ||||
| 				}	 | ||||
| 				if(json!=null) break; | ||||
| 						}					 | ||||
| 					}			 | ||||
| 				} | ||||
| 				if(credential!=null) break; | ||||
| 			} | ||||
| 		}	 | ||||
| 		if(json == null) throw new IllegalArgumentException("No credential inside SVG");		 | ||||
| 		return fromString(json, context); | ||||
| 		if(credential == null) throw new IllegalArgumentException("No credential inside SVG");	 | ||||
| 		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  | ||||
| 	 */ | ||||
| 	private JsonNode fromJson(Resource resource, RunContext context) throws Exception { | ||||
| 		return fromString(resource.asByteSource().asCharSource(UTF_8).read(), context); | ||||
| 	private Credential fromJson(Resource resource, RunContext context) throws Exception { | ||||
| 		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. | ||||
| 	 */ | ||||
| 	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); | ||||
| 	} | ||||
| 	 | ||||
| 	/** | ||||
| 	 * 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 | ||||
| 	 * @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); | ||||
| 		if(parts.size() != 3) throw new IllegalArgumentException("invalid jwt"); | ||||
| 		 | ||||
| 		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 jwsSignature = new String(decoder.decode(parts.get(2))); | ||||
| 				 | ||||
| 		//TODO @Miles | ||||
| 		 | ||||
| 		return null;	 | ||||
| 
 | ||||
| 		//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; | ||||
| 	} | ||||
| 	 | ||||
| 	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 | ||||
| 	void testSimplePNGPlainValid() { | ||||
| 		assertDoesNotThrow(()->{ | ||||
| @ -42,7 +41,6 @@ public class OB30Tests { | ||||
| 		});	 | ||||
| 	} | ||||
| 	 | ||||
| 	@Disabled | ||||
| 	@Test | ||||
| 	void testSimplePNGJWTValid() { | ||||
| 		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"?> | ||||
| <!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"> | ||||
|     <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> | ||||
|         <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" /> | ||||
|  | ||||
| Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.9 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user