def test_node_json_validation(self): node = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.com/1', 'type': 'Assertion', 'schema:location': { '@context': 'https://w3id.org/openbadges/extensions/geoCoordinatesExtension/context.json', 'type': ['Extension', 'extensions:GeoCoordinates'], 'description': 'That place in the woods where we built the fort', 'schema:geo': { 'schema:latitude': 44.580900, 'schema:longitude': -123.301815 } } } loader = CachableDocumentLoader(use_cache=True) loader.session.cache.remove_old_entries(datetime.datetime.utcnow()) loader.contexts = set() options = {'jsonld_options': {'documentLoader': loader}} set_up_context_mock() loader(OPENBADGES_CONTEXT_V2_URI) schema_url = list(GeoLocation.validation_schema)[0] responses.add(responses.GET, GeoLocation.context_url, status=200, json=GeoLocation.context_json) loader(GeoLocation.context_url) responses.add(responses.GET, schema_url, status=200, json=GeoLocation.validation_schema[schema_url]) loader.session.get(schema_url) state = INITIAL_STATE task = add_task(INTAKE_JSON, data=json.dumps(node), node_id=node['id']) result, message, actions = task_named(INTAKE_JSON)(state, task, **options) state = main_reducer(state, actions[0]) result, message, actions = task_named(actions[1]['name'])( state, actions[1], **options) # JSONLD_COMPACT_DATE state = main_reducer(state, actions[0]) # ADD_NODE task_meta = actions[1] # VALIDATE_EXTENSION_NODE result, message, actions = validate_extension_node(state, task_meta) self.assertTrue(result, "A valid expression of the extension should pass") self.assertIn('validated on node', message) self.assertEqual(len(actions), 0) del state['graph'][0]['schema:location']['schema:geo'][ 'schema:latitude'] result, message, actions = validate_extension_node(state, task_meta) self.assertFalse( result, "A required property not present should be detected by JSON-schema." )
def test_queue_validation_on_unknown_extension(self): set_up_context_mock() self.set_up_test_extension() first_node_json = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/assertion', 'extensions:exampleExtension': { '@context': self.extension_context_url, 'type': ['Extension', 'extensions:UnknownExtension'], 'unknownProperty': 'I\'m a property, short and sweet' }, 'evidence': 'http://example.org/evidence' } state = INITIAL_STATE task_meta = add_task( INTAKE_JSON, data=json.dumps(first_node_json), node_id=first_node_json['id']) result, message, actions = task_named(INTAKE_JSON)(state, task_meta) for action in actions: state = main_reducer(state, action) # Compact JSON result, message, actions = task_named(state['tasks'][0]['name'])(state, state['tasks'][0]) self.assertEqual(len(actions), 3) state = main_reducer(state, actions[0]) validation_action = actions[1] result, message, actions = validate_extension_node(state, validation_action) self.assertTrue(result)
def load_mocks(self): loader = CachableDocumentLoader(use_cache=True) loader.session.cache.remove_old_entries(datetime.datetime.utcnow()) loader.contexts = set() self.options = {'jsonld_options': {'documentLoader': loader}} set_up_context_mock() loader(OPENBADGES_CONTEXT_V2_URI) schema_url = list(ExampleExtension.validation_schema)[0] responses.add(responses.GET, ExampleExtension.context_url, status=200, json=ExampleExtension.context_json) loader(ExampleExtension.context_url) responses.add(responses.GET, schema_url, status=200, json=ExampleExtension.validation_schema[schema_url]) loader.session.get(schema_url) self.state = INITIAL_STATE task = add_task(INTAKE_JSON, data=json.dumps(self.first_node), node_id=self.first_node['id']) result, message, actions = task_named(INTAKE_JSON)(self.state, task, **self.options) self.state = main_reducer(self.state, actions[0]) result, message, actions = task_named(actions[1]['name'])( self.state, actions[1], **self.options) # JSONLD_COMPACT_DATE self.state = main_reducer(self.state, actions[0]) # ADD_NODE self.validation_task = actions[1] # VALIDATE_EXTENSION_NODE
def validate_basic_standalone_extension_node(self): set_up_context_mock() self.set_up_test_extension() extension_data = { '@context': self.extension_context_url, '@type': ['Extension', 'extensions:UnknownExtension'], 'unknownProperty': 'I\'m a property, short and sweet' } store = extension_validation_store(extension_data) report = generate_report(store) self.assertTrue(report['report']['valid']) extension_data[ 'unknownProperty'] = 42 # provokes extension schema error: should be a string. store = extension_validation_store(extension_data) report = generate_report(store) self.assertFalse(report['report']['valid']) del extension_data['@type'] extension_data['unknownProperty'] = "Ok, valid string again" store = extension_validation_store(extension_data) report = generate_report(store) self.assertFalse( report['report']['valid'], "Should report an error if there weren't any discoverable extension types to test." )
def test_extension_discovered_jsonld_compact(self): """ Ensure an extension node is properly discovered and that the task runs without error. """ node = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.com/1', 'type': 'Assertion', 'schema:location': { '@context': GeoLocation.context_url, 'type': ['Extension', 'extensions:GeoCoordinates'], 'description': 'That place in the woods where we built the fort', 'schema:geo': { 'schema:latitude': 44.580900, 'schema:longitude': -123.301815 } } } state = INITIAL_STATE set_up_context_mock() responses.add(responses.GET, GeoLocation.context_url, body=json.dumps(GeoLocation.context_json), status=200, content_type='application/ld+json') schema_url = 'https://w3id.org/openbadges/extensions/geoCoordinatesExtension/schema.json' responses.add(responses.GET, schema_url, body=json.dumps( GeoLocation.validation_schema[schema_url]), status=200, content_type='application/ld+json') compact_task = add_task(JSONLD_COMPACT_DATA, data=json.dumps(node), jsonld_options=jsonld_no_cache, context_urls=[GeoLocation.context_url]) result, message, actions = task_named(JSONLD_COMPACT_DATA)( state, compact_task) self.assertTrue(result, "JSON-LD Compact is successful.") self.assertIn(VALIDATE_EXTENSION_NODE, [i.get('name') for i in actions], "Validation task queued.") state = main_reducer(state, actions[0]) # ADD_NODE validate_task = [ i for i in actions if i.get('name') == VALIDATE_EXTENSION_NODE ][0] self.assertIsNotNone(validate_task['node_json']) result, message, actions = task_named(VALIDATE_EXTENSION_NODE)( state, validate_task) self.assertTrue(result, "Validation task is successful.")
def test_can_full_verify_with_revocation_check(self): input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} set_up_image_mock(u'https://example.org/beths-robot-badge.png') input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) set_up_image_mock(input_badgeclass['image']) revocation_list = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/revocationList', 'type': 'RevocationList', 'revokedAssertions': []} input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['revocationList'] = revocation_list['id'] input_issuer['publicKey'] = input_assertion['verification']['creator'] private_key = RSA.generate(2048) print("PKEY") print(private_key.publickey().exportKey('PEM').decode()) cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': private_key.publickey().exportKey('PEM').decode() } set_up_context_mock() for doc in [input_assertion, input_badgeclass, input_issuer, cryptographic_key_doc, revocation_list]: responses.add(responses.GET, doc['id'], json=doc, status=200) header = json.dumps({'alg': 'RS256'}) payload = json.dumps(input_assertion) encoded_separator = '.' if not sys.version[:3] < '3': encoded_separator = '.'.encode() encoded_header = b64encode(header.encode()) encoded_payload = b64encode(payload.encode()) else: encoded_header = b64encode(header) encoded_payload = b64encode(payload) signature = encoded_separator.join([ encoded_header, encoded_payload, jws.sign(header, payload, private_key, is_json=True) ]) response = verify(signature, use_cache=False) self.assertTrue(response['report']['valid'])
def test_detect_jws_signed_input_type(self): set_up_context_mock() # responses.add(responses.GET, badgeclass_data['id'], json=badgeclass_data, status=200) # responses.add(responses.GET, issuer_data['id'], json=issuer_data, status=200) # responses.add(responses.GET, signing_key['id'], json=signing_key, status=200) state = INITIAL_STATE.copy() state['input']['value'] = self.signed_assertion success, message, actions = detect_input_type(state) self.assertTrue(success) self.assertEqual(len(actions), 2) self.assertEqual(actions[0]['input_type'], 'jws')
def test_revoked_badge_marked_invalid(self): input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) revocation_list = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/revocationList', 'type': 'RevocationList', 'revokedAssertions': [ {'id': input_assertion['id'], 'revocationReason': 'A good reason, for sure'}, {'id': 'urn:uuid:52e4c6b3-8c13-4fa8-8482-a5cf34ef37a9'}, 'urn:uuid:6deb4a00-ebce-4b28-8cc2-afa705ef7be4' ] } input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['revocationList'] = revocation_list['id'] input_issuer['publicKey'] = input_assertion['verification']['creator'] key = RSA.generate(2048) public_key_pem = key.publickey().export_key() cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': public_key_pem.decode() } set_up_context_mock() for doc in [input_assertion, input_badgeclass, input_issuer, cryptographic_key_doc, revocation_list]: responses.add(responses.GET, doc['id'], json=doc, status=200) signature = jws.sign(input_assertion, key, algorithm='RS256') response = verify(signature, use_cache=False) self.assertFalse(response['report']['valid']) msg = [a for a in response['report']['messages'] if a.get('name') == VERIFY_SIGNED_ASSERTION_NOT_REVOKED][0] self.assertIn('A good reason', msg['result']) # Assert pruning went well to eliminate revocationlist revokedAssertions except for the revoked one rev_list = [n for n in response['graph'] if n.get('id') == revocation_list['id']][0] self.assertEqual(len(rev_list['revokedAssertions']), 1) self.assertEqual(rev_list['revokedAssertions'], [revocation_list['revokedAssertions'][0]])
def set_response_mocks(): # Make sure to add @responses.activate decorator in calling method set_up_context_mock() responses.add( responses.GET, 'https://example.org/beths-robotics-badge.json', body=test_components['2_0_basic_assertion'], status=200, content_type='application/ld+json' ) responses.add( responses.GET, 'https://example.org/robotics-badge.json', body=test_components['2_0_basic_badgeclass'], status=200, content_type='application/ld+json' ) responses.add( responses.GET, 'https://example.org/organization.json', body=test_components['2_0_basic_issuer'], status=200, content_type='application/ld+json' )
def test_that_pyld_accepts_caching_loader_for_compaction(self): assertion_data = json.loads(test_components['2_0_basic_assertion']) context_url = assertion_data['@context'] loadurl = CachableDocumentLoader(use_cache=True) set_up_context_mock() first_compacted = jsonld.compact(assertion_data, context_url, options={'documentLoader': loadurl}) second_compacted = jsonld.compact(assertion_data, context_url, options={'documentLoader': loadurl}) # in order to have 'HostedBadge' as the verification type, the assertion_data # needs to have gone through compaction against the openbadges context document self.assertEqual(first_compacted['verification']['type'], 'HostedBadge') # second compaction should have built from the cache self.assertEqual(first_compacted['verification']['type'], second_compacted['verification']['type'])
def test_loader_with_session(self): session = CachedSession(backend='memory', expire_after=100000) loadurl = CachableDocumentLoader(use_cache=True, session=session) assertion_data = json.loads(test_components['2_0_basic_assertion']) context_url = assertion_data['@context'] set_up_context_mock() session.get(context_url) # precache response responses.reset() responses.add(responses.GET, context_url, json={'nothing': 'happenin'}) first_compacted = jsonld.compact(assertion_data, context_url, options={'documentLoader': loadurl}) second_compacted = jsonld.compact(assertion_data, context_url, options={'documentLoader': loadurl}) # second compaction should have built from the cache self.assertEqual(first_compacted['verification']['type'], second_compacted['verification']['type'])
def test_can_full_verify_jws_signed_assertion(self): """ I can input a JWS string I can extract the Assertion from the input signature string and store it as the canonical version of the Assertion. I can discover and retrieve key information from the Assertion. I can Access the signing key I can verify the key is associated with the listed issuer Profile I can verify the JWS signature has been created by a key trusted to correspond to the issuer Profile Next: I can verify an assertion with an ephemeral embedded badgeclass as well """ input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} set_up_image_mock(u'https://example.org/beths-robot-badge.png') input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) set_up_image_mock(input_badgeclass['image']) input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['publicKey'] = input_assertion['verification']['creator'] key = RSA.generate(2048) public_key_pem = key.publickey().export_key() cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': public_key_pem.decode() } set_up_context_mock() for doc in [input_badgeclass, input_issuer, cryptographic_key_doc]: responses.add(responses.GET, doc['id'], json=doc, status=200) signature = jws.sign(input_assertion, key, algorithm='RS256') response = verify(signature, use_cache=False) self.assertTrue(response['report']['valid'])
def test_can_full_verify_with_revocation_check(self): input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} set_up_image_mock(u'https://example.org/beths-robot-badge.png') input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) set_up_image_mock(input_badgeclass['image']) revocation_list = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/revocationList', 'type': 'RevocationList', 'revokedAssertions': []} input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['revocationList'] = revocation_list['id'] input_issuer['publicKey'] = input_assertion['verification']['creator'] key = RSA.generate(2048) public_key_pem = key.publickey().export_key() cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': public_key_pem.decode() } set_up_context_mock() for doc in [input_assertion, input_badgeclass, input_issuer, cryptographic_key_doc, revocation_list]: responses.add(responses.GET, doc['id'], json=doc, status=200) header = {'alg': 'RS256'} payload = json.dumps(input_assertion).encode() signature = jws.sign(input_assertion, key, algorithm='RS256') response = verify(signature, use_cache=False) self.assertTrue(response['report']['valid'])
def test_fetch_task_handles_potential_baked_input(self): set_up_context_mock() assertion_url = 'http://example.org/assertion/1' image_url = 'http://example.org/image' with open( os.path.join(os.path.dirname(__file__), 'testfiles', 'public_domain_heart.png'), 'rb') as f: baked_file = bake(f, assertion_url) responses.add(responses.GET, image_url, body=baked_file.read(), status=200, content_type='image/png') task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=True) result, message, actions = run_task({}, task) self.assertTrue(result) store_resource_action = [ a for a in actions if a.get('type') == STORE_ORIGINAL_RESOURCE ][0] process_baked_input_action = [ a for a in actions if a.get('name') == PROCESS_BAKED_RESOURCE ][0] self.assertEqual(store_resource_action.get('node_id'), image_url) self.assertEqual(process_baked_input_action.get('node_id'), image_url) task = add_task(FETCH_HTTP_NODE, url=image_url, is_potential_baked_input=False) result, message, actions = run_task({}, task) self.assertTrue(result)
def test_queue_validation_on_unknown_extension(self): set_up_context_mock() extension_schema = { "$schema": "http://json-schema.org/draft-04/schema#", "title": "1.1 Open Badge Example Extension for testing: Unknown Extension", "description": "An extension that allows you to add a single string unknownProperty to an extension object for unknown reasons.", "type": "object", "properties": { "unknownProperty": { "type": "string" } }, "required": ["unknownProperty"] } extension_schema_url = 'http://example.org/unkownSchema' extension_context = { '@context': { "obi": "https://w3id.org/openbadges#", "extensions": "https://w3id.org/openbadges/extensions#", 'unknownProperty': 'http://schema.org/unknownProperty' }, "obi:validation": [ { "obi:validatesType": "extensions:UnknownExtension", "obi:validationSchema": extension_schema_url } ] } extension_context_url = 'http://example.org/unknownExtensionContext' first_node_json = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/assertion', 'extensions:exampleExtension': { '@context': extension_context_url, 'type': ['Extension', 'extensions:UnknownExtension'], 'unknownProperty': 'I\'m a property, short and sweet' }, 'evidence': 'http://example.org/evidence' } responses.add( responses.GET, extension_context_url, json=extension_context ) responses.add( responses.GET, extension_schema_url, json=extension_schema ) state = INITIAL_STATE task_meta = add_task( INTAKE_JSON, data=json.dumps(first_node_json), node_id=first_node_json['id']) result, message, actions = task_named(INTAKE_JSON)(state, task_meta) for action in actions: state = main_reducer(state, action) # Compact JSON result, message, actions = task_named(state['tasks'][0]['name'])(state, state['tasks'][0]) self.assertEqual(len(actions), 3) state = main_reducer(state, actions[0]) validation_action = actions[1] result, message, actions = validate_extension_node(state, validation_action) self.assertTrue(result)
def test_can_full_verify_jws_signed_assertion(self): """ I can input a JWS string I can extract the Assertion from the input signature string and store it as the canonical version of the Assertion. I can discover and retrieve key information from the Assertion. I can Access the signing key I can verify the key is associated with the listed issuer Profile I can verify the JWS signature has been created by a key trusted to correspond to the issuer Profile Next: I can verify an assertion with an ephemeral embedded badgeclass as well """ input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} set_up_image_mock(u'https://example.org/beths-robot-badge.png') input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) set_up_image_mock(input_badgeclass['image']) input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['publicKey'] = input_assertion['verification']['creator'] private_key = RSA.generate(2048) cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': private_key.publickey().exportKey('PEM').decode() } set_up_context_mock() for doc in [input_assertion, input_badgeclass, input_issuer, cryptographic_key_doc]: responses.add(responses.GET, doc['id'], json=doc, status=200) header = json.dumps({'alg': 'RS256'}) payload = json.dumps(input_assertion) encoded_separator = '.' if not sys.version[:3] < '3': encoded_separator = '.'.encode() encoded_header = b64encode(header.encode()) encoded_payload = b64encode(payload.encode()) else: encoded_header = b64encode(header) encoded_payload = b64encode(payload) signature = encoded_separator.join([ encoded_header, encoded_payload, jws.sign(header,payload,private_key, is_json=True) ]) response = verify(signature, use_cache=False) print("TEST CAN FULLY VERIFY JWS SIGNED ASSERTION : response:") print(response['report']) self.assertTrue(response['report']['valid'])
def test_revoked_badge_marked_invalid(self): input_assertion = json.loads(test_components['2_0_basic_assertion']) input_assertion['verification'] = {'type': 'signed', 'creator': 'http://example.org/key1'} input_badgeclass = json.loads(test_components['2_0_basic_badgeclass']) revocation_list = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': 'http://example.org/revocationList', 'type': 'RevocationList', 'revokedAssertions': [ {'id': input_assertion['id'], 'revocationReason': 'A good reason, for sure'}, {'id': 'urn:uuid:52e4c6b3-8c13-4fa8-8482-a5cf34ef37a9'}, 'urn:uuid:6deb4a00-ebce-4b28-8cc2-afa705ef7be4' ] } input_issuer = json.loads(test_components['2_0_basic_issuer']) input_issuer['revocationList'] = revocation_list['id'] input_issuer['publicKey'] = input_assertion['verification']['creator'] private_key = RSA.generate(2048) cryptographic_key_doc = { '@context': OPENBADGES_CONTEXT_V2_URI, 'id': input_assertion['verification']['creator'], 'type': 'CryptographicKey', 'owner': input_issuer['id'], 'publicKeyPem': make_string_from_bytes(private_key.publickey().exportKey('PEM')) } set_up_context_mock() for doc in [input_assertion, input_badgeclass, input_issuer, cryptographic_key_doc, revocation_list]: responses.add(responses.GET, doc['id'], json=doc, status=200) header = json.dumps({'alg': 'RS256'}) payload = json.dumps(input_assertion) encoded_separator = '.' if not sys.version[:3] < '3': encoded_separator = '.'.encode() encoded_header = b64encode(header.encode()) encoded_payload = b64encode(payload.encode()) else: encoded_header = b64encode(header) encoded_payload = b64encode(payload) signature = encoded_separator.join([ encoded_header, encoded_payload, jws.sign(header, payload, private_key, is_json=True) ]) response = verify(signature, use_cache=False) self.assertFalse(response['report']['valid']) msg = [a for a in response['report']['messages'] if a.get('name') == VERIFY_SIGNED_ASSERTION_NOT_REVOKED][0] self.assertIn('A good reason', msg['result']) # Assert pruning went well to eliminate revocationlist revokedAssertions except for the revoked one rev_list = [n for n in response['graph'] if n.get('id') == revocation_list['id']][0] self.assertEqual(len(rev_list['revokedAssertions']), 1) self.assertEqual(rev_list['revokedAssertions'], [revocation_list['revokedAssertions'][0]])