diff --git a/examples/passwd.py b/examples/passwd.py
index e0fa413e6552c3a504c1aab5e853b69f0b7864e6..7a516eb8ae05c5ce2e549154009990e6a54fb592 100644
--- a/examples/passwd.py
+++ b/examples/passwd.py
@@ -1,11 +1,14 @@
+import logging
 import socketserver
 import pwd
 import grp
 
 import ldapserver
 
+logging.basicConfig(level=logging.INFO)
+
 class RequestHandler(ldapserver.LDAPRequestHandler):
-	subschema = ldapserver.schema.RFC2307BIS_SUBSCHEMA
+	subschema = ldapserver.SubschemaSubentry(ldapserver.schema.RFC2307BIS_SCHEMA, 'cn=Subschema')
 
 	def do_search(self, basedn, scope, filterobj):
 		yield from super().do_search(basedn, scope, filterobj)
@@ -17,8 +20,8 @@ class RequestHandler(ldapserver.LDAPRequestHandler):
 		for user in pwd.getpwall():
 			user_gids[user.pw_gid] = user_gids.get(user.pw_gid, set()) | {user.pw_name}
 			yield self.subschema.Object(ldapserver.dn.DN('ou=users,dc=example,dc=com', uid=user.pw_name), **{
-				'objectClass': ['top', 'inetorgperson', 'organizationalperson', 'person', 'posixaccount'],
-				'structuralObjectClass': ['organization'],
+				'objectClass': ['top', 'organizationalperson', 'person', 'posixaccount'],
+				'structuralObjectClass': ['organizationalperson'],
 				'uid': [user.pw_name],
 				'uidNumber': [user.pw_uid],
 				'gidNumber': [user.pw_gid],
@@ -27,8 +30,8 @@ class RequestHandler(ldapserver.LDAPRequestHandler):
 		for group in grp.getgrall():
 			members = set(group.gr_mem) | user_gids.get(group.gr_gid, set())
 			yield self.subschema.Object(ldapserver.dn.DN('ou=groups,dc=example,dc=com', cn=group.gr_name), **{
-				'objectClass': ['structuralobjectclass', 'objectclass', 'cn', 'description', 'gidnumber', 'uniquemember'],
-				'structuralObjectClass': ['organization'],
+				'objectClass': ['top', 'groupOfUniqueNames', 'posixGroup'],
+				'structuralObjectClass': ['groupOfUniqueNames'],
 				'cn': [group.gr_name],
 				'gidNumber': [group.gr_gid],
 				'uniqueMember': [ldapserver.dn.DN('ou=user,dc=example,dc=com', uid=name) for name in members],
diff --git a/examples/static.py b/examples/static.py
deleted file mode 100644
index 3052141172459b5079da2345dff7a5ad17bff7c3..0000000000000000000000000000000000000000
--- a/examples/static.py
+++ /dev/null
@@ -1,16 +0,0 @@
-import socketserver
-import ldapserver
-
-class RequestHandler(ldapserver.LDAPRequestHandler):
-	subschema = ldapserver.schema.RFC2307BIS_SUBSCHEMA
-
-	static_objects = [
-		subschema.Object('dc=example,dc=com',
-		                 objectClass=['top', 'dcObject', 'organization'],
-		                 structuralObjectClass=['organization'],
-										 cn=['foo', 'bar'],
-										 c=['test'])
-	]
-
-if __name__ == '__main__':
-	socketserver.ThreadingTCPServer(('127.0.0.1', 3890), RequestHandler).serve_forever()
diff --git a/ldapserver/dn.py b/ldapserver/dn.py
index bcb77370139575e2bac62f730a63fe30bca0e24c..8e4ded8abbe5c0255db3a9f477dd1104b8a5cfcb 100644
--- a/ldapserver/dn.py
+++ b/ldapserver/dn.py
@@ -17,6 +17,7 @@ Limitations:
 
 import typing
 import unicodedata
+import re
 from string import hexdigits as HEXDIGITS
 
 __all__ = ['DN', 'RDN', 'RDNAssertion']
@@ -62,9 +63,6 @@ class DN(tuple):
 	def __str__(self):
 		return ','.join(map(str, self))
 
-	def __bytes__(self):
-		return str(self).encode('utf8')
-
 	def __repr__(self):
 		return '<ldapserver.dn.DN %s>'%str(self)
 
@@ -121,6 +119,35 @@ class DN(tuple):
 			return None
 		return self[0].value_normalized # pylint: disable=no-member
 
+class DNWithUID(DN):
+	# pylint: disable=arguments-differ,no-member
+	def __new__(cls, dn, uid=None):
+		if not uid:
+			return dn
+		if not re.fullmatch(r"'[01]*'B", uid):
+			raise ValueError('Invalid uid value')
+		obj = super().__new__(cls, *dn)
+		obj.uid = uid
+		return obj
+
+	@classmethod
+	def from_str(cls, expr):
+		dn_part, uid_part = (expr.rsplit('#', 1) + [''])[:2]
+		return cls(DN.from_str(dn_part), uid_part or None)
+
+	def __str__(self):
+		return super().__str__() + '#' + self.uid
+
+	def __repr__(self):
+		return '<ldapserver.dn.DNWithUID %s>'%str(self)
+
+	def __eq__(self, obj):
+		return type(self) is type(obj) and super().__eq__(obj) and self.uid == obj.uid
+
+	@property
+	def dn(self):
+		return DN(*self)
+
 class RDN(tuple):
 	'''Relative Distinguished Name consisting of one or more `RDNAssertion` objects'''
 	def __new__(cls, *assertions, **kwargs):
diff --git a/ldapserver/ldap.py b/ldapserver/ldap.py
index 639d715abc76f9c74f81dfd7bee3176e570573cc..91d5ccdc5d9f582a9c0a58e1de8063aa30919a2d 100644
--- a/ldapserver/ldap.py
+++ b/ldapserver/ldap.py
@@ -358,7 +358,7 @@ class LDAPResultCode(enum.Enum):
 class LDAPResult(asn1.Sequence):
 	BER_TAG = (5, True, 1)
 	SEQUENCE_FIELDS = [
-		(asn1.wrapenum(LDAPResultCode), 'resultCode', None, False),
+		(asn1.wrapenum(LDAPResultCode), 'resultCode', LDAPResultCode.success, False),
 		(LDAPString, 'matchedDN', '', False),
 		(LDAPString, 'diagnosticMessage', '', False),
 	]
diff --git a/ldapserver/objects.py b/ldapserver/objects.py
index 6140f96d0881458ee6c21d83ef62fac78579ce56..5b930e47400753c13d134f855cd86c0b2d45cc0c 100644
--- a/ldapserver/objects.py
+++ b/ldapserver/objects.py
@@ -1,56 +1,41 @@
+import collections.abc
 import enum
 
 from . import ldap, exceptions
 from .dn import DN
 
-class FilterResult(enum.Enum):
-	TRUE = enum.auto()
-	FALSE = enum.auto()
-	UNDEFINED = enum.auto()
-	MAYBE_TRUE = enum.auto() # used by ObjectTemplate
-
-def match_to_filter_result(match_result):
-	if match_result is True:
-		return FilterResult.TRUE
-	if match_result is False:
-		return FilterResult.FALSE
-	return FilterResult.UNDEFINED
-
-def any_3value(iterable):
-	'''Extended three-valued logic equivalent of any builtin
-
-	If all items are TRUE, return TRUE. Otherwise if any item is MAYBE_TRUE,
-	return MAYBE_TRUE. If neither TRUE nor MAYBE_TRUE are in items, but any
-	item is UNDEFINED, return UNDEFINED. Otherwise (all items are FALSE),
-	return FALSE.'''
-	result = FilterResult.FALSE
-	for item in iterable:
-		if item == FilterResult.TRUE:
-			return FilterResult.TRUE
-		elif item == FilterResult.MAYBE_TRUE:
-			result = FilterResult.MAYBE_TRUE
-		elif item == FilterResult.UNDEFINED and result == FilterResult.FALSE:
-			result = FilterResult.UNDEFINED
-	return result
-
-def all_3value(iterable):
-	'''Extended three-valued logic equivalent of all builtin
-
-	If all items are TRUE, return TRUE. If any item is FALSE, return FALSE.
-	If no item is FALSE and any item is UNDEFINED, return UNDEFINED.
-	Otherwise (not item is FALSE or UNDEFINED and not all items are TRUE,
-	so at least one item is MAYBE_TRUE), return MAYBE_TRUE.'''
-	result = FilterResult.TRUE
-	for item in iterable:
-		if item == FilterResult.FALSE:
-			return FilterResult.FALSE
-		elif item == FilterResult.UNDEFINED:
-			result = FilterResult.UNDEFINED
-		elif item == FilterResult.MAYBE_TRUE and result == FilterResult.TRUE:
-			result = FilterResult.MAYBE_TRUE
-	return result
-
-class AttributeDict(dict):
+class TypeKeysView(collections.abc.Set):
+	def __init__(self, attributes):
+		self.__attributes = attributes
+
+	def __iter__(self):
+		for attribute_type, values in self.__attributes.items():
+			if values:
+				yield attribute_type
+
+	def __len__(self):
+		return len(list(iter(self)))
+
+	def __contains__(self, value):
+		return bool(self.__attributes.get(value))
+
+class TypeItemsView(collections.abc.Set):
+	def __init__(self, attributes):
+		self.__attributes = attributes
+
+	def __iter__(self):
+		for attribute_type, values in self.__attributes.items():
+			if values:
+				yield attribute_type, values
+
+	def __len__(self):
+		return len(list(iter(self)))
+
+	def __contains__(self, value):
+		key, values = value
+		return self.__attributes.get(key) == values
+
+class AttributeDict(collections.abc.MutableMapping):
 	'''Special dictionary holding LDAP attribute values
 
 	Attribute values can be set and accessed by their attribute type's numeric
@@ -59,158 +44,75 @@ class AttributeDict(dict):
 	attribute behaves the same as accessing an empty attribute. List items must
 	conform to the attribute's syntax.'''
 	def __init__(self, schema, **attributes):
-		super().__init__()
+		self.__attributes = {}
 		self.schema = schema
-		for key, value in attributes.items():
-			self[key] = value
+		for key, values in attributes.items():
+			self[key] = values
 
-	def __contains__(self, key):
-		try:
-			return super().__contains__(self.schema.get_attribute_type(key).name)
-		except KeyError:
-			return False
+	def __getitem__(self, key):
+		return self.__attributes.setdefault(self.schema.attribute_types[key], [])
 
 	def __setitem__(self, key, values):
-		super().__setitem__(self.schema.get_attribute_type(key).name, values)
+		self.__attributes[self.schema.attribute_types[key]] = values
 
-	def __getitem__(self, key):
-		canonical_oid = self.schema.get_attribute_type(key).name
-		if not super().__contains__(canonical_oid):
-			super().__setitem__(canonical_oid, [])
-		result = super().__getitem__(canonical_oid)
-		if callable(result):
-			return result()
-		return result
+	def __delitem__(self, key):
+		self[key] = []
 
-	def setdefault(self, key, default=None):
-		canonical_oid = self.schema.get_attribute_type(key).name
-		return super().setdefault(canonical_oid, default)
+	def __iter__(self):
+		for attribute_type, values in self.__attributes.items():
+			if values:
+				yield attribute_type.ref
 
-	def get(self, key, default=None):
-		return self[key] or default
+	def __len__(self):
+		return len(list(iter(self)))
 
-	def get_with_subtypes(self, key):
+	def get(self, key, default=None, subtypes=False): # pylint: disable=arguments-differ
+		attribute_type = self.schema.attribute_types.get(key)
+		attribute_types = [attribute_type]
+		if subtypes and attribute_type:
+			attribute_types += attribute_type.subtypes
 		result = []
-		for attribute_type in self.schema.get_attribute_type_and_subtypes(key):
-			result += self[attribute_type.name]
+		for attribute_type in attribute_types:
+			result += self.__attributes.get(attribute_type, [])
+		if not result:
+			result = default if default is not None else []
 		return result
 
-	def match_present(self, key):
-		attribute_type = self.schema.get_attribute_type(key)
-		if attribute_type is None:
-			return FilterResult.UNDEFINED
-		if self[attribute_type.name] != []:
-			return FilterResult.TRUE
-		else:
-			return FilterResult.FALSE
-
-	def match_equal(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.equality.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.equality.match_equal(self.schema, attrval, assertion_value)), self.get_with_subtypes(key)))
-
-	def match_substr(self, key, inital_substring, any_substrings, final_substring):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None or attribute_type.substr is None:
-			return FilterResult.UNDEFINED
-		if inital_substring:
-			inital_substring = attribute_type.equality.syntax.decode(self.schema, inital_substring)
-			if inital_substring is None:
-				return FilterResult.UNDEFINED
-		any_substrings = [attribute_type.equality.syntax.decode(self.schema, substring) for substring in any_substrings]
-		if None in any_substrings:
-			return FilterResult.UNDEFINED
-		if final_substring:
-			final_substring = attribute_type.equality.syntax.decode(self.schema, final_substring)
-			if final_substring is None:
-				return FilterResult.UNDEFINED
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.substr.match_substr(self.schema, attrval, inital_substring, any_substrings, final_substring)), self.get_with_subtypes(key)))
-
-	def match_approx(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.equality.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.equality.match_approx(self.schema, attrval, assertion_value)), self.get_with_subtypes(key)))
+	def keys(self, types=False): # pylint: disable=arguments-differ
+		if not types:
+			return super().keys()
+		return TypeKeysView(self.__attributes)
 
-	def match_greater_or_equal(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.ordering is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.ordering.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.ordering.match_greater_or_equal(self.schema, attrval, assertion_value)), self.get_with_subtypes(key)))
+	def items(self, types=False): # pylint: disable=arguments-differ
+		if not types:
+			return super().items()
+		return TypeItemsView(self.__attributes)
 
-	def match_less(self, key, assertion_value):
+	def __contains__(self, key):
 		try:
-			attribute_type = self.schema.get_attribute_type(key)
+			return bool(self[key])
 		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.ordering is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.ordering.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.ordering.match_less(self.schema, attrval, assertion_value)), self.get_with_subtypes(key)))
+			return False
 
-	def match_less_or_equal(self, key, assertion_value):
-		return any_3value((self.match_equal(key, assertion_value),
-		                   self.match_less(key, assertion_value)))
+	def setdefault(self, key, default=False):
+		if key in self:
+			return self[key]
+		if default is None:
+			default = []
+		self[key] = default
+		return default
 
-	def match_filter(self, filter_obj):
-		if isinstance(filter_obj, ldap.FilterAnd):
-			return all_3value(map(self.match_filter, filter_obj.filters))
-		elif isinstance(filter_obj, ldap.FilterOr):
-			return any_3value(map(self.match_filter, filter_obj.filters))
-		elif isinstance(filter_obj, ldap.FilterNot):
-			subresult = self.match_filter(filter_obj.filter)
-			if subresult == FilterResult.TRUE:
-				return FilterResult.FALSE
-			elif subresult == FilterResult.FALSE:
-				return FilterResult.TRUE
-			else:
-				return subresult
-		elif isinstance(filter_obj, ldap.FilterPresent):
-			return self.match_present(filter_obj.attribute)
-		elif isinstance(filter_obj, ldap.FilterEqual):
-			return self.match_equal(filter_obj.attribute, filter_obj.value)
-		elif isinstance(filter_obj, ldap.FilterSubstrings):
-			return self.match_substr(filter_obj.attribute, filter_obj.initial_substring,
-			                         filter_obj.any_substrings, filter_obj.final_substring)
-		elif isinstance(filter_obj, ldap.FilterApproxMatch):
-			return self.match_approx(filter_obj.attribute, filter_obj.value)
-		elif isinstance(filter_obj, ldap.FilterGreaterOrEqual):
-			return self.match_greater_or_equal(filter_obj.attribute, filter_obj.value)
-		elif isinstance(filter_obj, ldap.FilterLessOrEqual):
-			return self.match_less_or_equal(filter_obj.attribute, filter_obj.value)
-		else:
-			return FilterResult.UNDEFINED
+class FilterResult(enum.Enum):
+	TRUE = enum.auto()
+	FALSE = enum.auto()
+	UNDEFINED = enum.auto()
 
 class Object(AttributeDict):
 	def __init__(self, schema, dn, **attributes):
 		super().__init__(schema, **attributes)
 		self.dn = DN(dn)
 
-	def match_dn(self, basedn, scope):
+	def __search_match_dn(self, basedn, scope):
 		if scope == ldap.SearchScope.baseObject:
 			return self.dn == basedn
 		elif scope == ldap.SearchScope.singleLevel:
@@ -220,51 +122,139 @@ class Object(AttributeDict):
 		else:
 			return False
 
+	def __search_match_filter(self, filter_obj):
+		# pylint: disable=too-many-branches,too-many-return-statements,too-many-statements,too-many-nested-blocks
+		if isinstance(filter_obj, ldap.FilterAnd):
+			result = FilterResult.TRUE
+			for subfilter in filter_obj.filters:
+				subresult = self.__search_match_filter(subfilter)
+				if subresult == FilterResult.FALSE:
+					return FilterResult.FALSE
+				elif subresult == FilterResult.UNDEFINED:
+					result = FilterResult.UNDEFINED
+			return result
+		elif isinstance(filter_obj, ldap.FilterOr):
+			result = FilterResult.FALSE
+			for subfilter in filter_obj.filters:
+				subresult = self.__search_match_filter(subfilter)
+				if subresult == FilterResult.TRUE:
+					return FilterResult.TRUE
+				elif subresult == FilterResult.UNDEFINED:
+					result = FilterResult.UNDEFINED
+			return result
+		elif isinstance(filter_obj, ldap.FilterNot):
+			subresult = self.__search_match_filter(filter_obj.filter)
+			if subresult == FilterResult.TRUE:
+				return FilterResult.FALSE
+			elif subresult == FilterResult.FALSE:
+				return FilterResult.TRUE
+			else:
+				return subresult
+		elif isinstance(filter_obj, (ldap.FilterPresent,
+		                             ldap.FilterEqual,
+		                             ldap.FilterSubstrings,
+		                             ldap.FilterApproxMatch,
+		                             ldap.FilterGreaterOrEqual,
+		                             ldap.FilterLessOrEqual)):
+			try:
+				attribute_type = self.schema.attribute_types[filter_obj.attribute]
+			except KeyError:
+				return FilterResult.UNDEFINED
+			values = self.get(filter_obj.attribute, subtypes=True)
+			try:
+				if isinstance(filter_obj, ldap.FilterPresent):
+					result = values != []
+				elif isinstance(filter_obj, ldap.FilterEqual):
+					result = attribute_type.match_equal(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterSubstrings):
+					result = attribute_type.match_substr(values,
+							filter_obj.initial_substring, filter_obj.any_substrings, filter_obj.final_substring)
+				elif isinstance(filter_obj, ldap.FilterApproxMatch):
+					result = attribute_type.match_approx(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterGreaterOrEqual):
+					result = attribute_type.match_greater_or_equal(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterLessOrEqual):
+					result = attribute_type.match_less_or_equal(values, filter_obj.value)
+				else:
+					return FilterResult.UNDEFINED
+				return FilterResult.TRUE if result else FilterResult.FALSE
+			except exceptions.LDAPError:
+				return FilterResult.UNDEFINED
+		elif isinstance(filter_obj, ldap.FilterExtensibleMatch):
+			attribute_types = []
+			matching_rule = None
+			try:
+				if filter_obj.type is not None and filter_obj.matchingRule is not None:
+					attribute_types = [self.schema.attribute_types[filter_obj.type]]
+					matching_rule = self.schema.matching_rules[filter_obj.matchingRule]
+				elif filter_obj.type is not None:
+					attribute_types = [self.schema.attribute_types[filter_obj.type]]
+				elif filter_obj.matchingRule is not None:
+					matching_rule = self.schema.matching_rules[filter_obj.matchingRule]
+					attribute_types = matching_rule.compatible_attribute_types
+			except KeyError:
+				pass
+			result = FilterResult.FALSE
+			for attribute_type in attribute_types:
+				values = self.get(attribute_type.oid, subtypes=True)
+				if filter_obj.dnAttributes:
+					for rdn in self.dn:
+						for assertion in rdn:
+							if assertion.attribute.lower() == attribute_type.ref.lower():
+								values.append(assertion.value)
+				try:
+					if attribute_type.match_extensible(values, filter_obj.matchValue, matching_rule):
+						return FilterResult.TRUE
+				except exceptions.LDAPError:
+					result = FilterResult.UNDEFINED
+			return result
+		else:
+			return FilterResult.UNDEFINED
+
 	def match_search(self, base_obj, scope, filter_obj):
-		return self.match_dn(DN.from_str(base_obj), scope) and self.match_filter(filter_obj) == FilterResult.TRUE
+		return self.__search_match_dn(DN.from_str(base_obj), scope) and \
+		       self.__search_match_filter(filter_obj) == FilterResult.TRUE
 
-	def match_compare(self, attribute, value):
-		try:
-			attribute_type = self.schema.get_attribute_type(attribute)
-		except KeyError as exc:
-			raise exceptions.LDAPUndefinedAttributeType() from exc
-		if attribute_type.equality is None:
-			raise exceptions.LDAPInappropriateMatching()
-		value = attribute_type.equality.syntax.decode(self.schema, value)
-		if value is None:
-			raise exceptions.LDAPInvalidAttributeSyntax()
-		for attrval in self.get_with_subtypes(attribute):
-			if attribute_type.equality.match_equal(self.schema, attrval, value):
-				return True
-		return False
-
-	def get_search_result_entry(self, attributes=None, types_only=False):
+	def search(self, base_obj, scope, filter_obj, attributes, types_only):
+		if not self.match_search(base_obj, scope, filter_obj):
+			return None
 		selected_attributes = set()
 		for selector in attributes or ['*']:
 			if selector == '*':
-				selected_attributes |= set(self.schema.user_attribute_types)
+				selected_attributes |= self.schema.user_attribute_types
 			elif selector == '1.1':
 				continue
-			else:
-				try:
-					selected_attributes.add(self.schema.get_attribute_type(selector))
-				except KeyError:
-					pass
+			elif selector in self.schema.attribute_types:
+				selected_attributes.add(self.schema.attribute_types[selector])
 		partial_attributes = []
-		for attribute_type in selected_attributes:
-			values = self[attribute_type.name]
-			if values != []:
+		for attribute_type, values in self.items(types=True):
+			if attribute_type in selected_attributes:
 				if types_only:
 					values = []
-				partial_attributes.append(ldap.PartialAttribute(attribute_type.name, [attribute_type.syntax.encode(self.schema, value) for value in values]))
+				encoded_values = [attribute_type.encode(value) for value in values]
+				partial_attributes.append(ldap.PartialAttribute(attribute_type.ref, encoded_values))
 		return ldap.SearchResultEntry(str(self.dn), partial_attributes)
 
+	def compare(self, dn, attribute, value):
+		try:
+			dn = DN.from_str(dn)
+		except ValueError as exc:
+			raise exceptions.LDAPNoSuchObject() from exc
+		if dn != self.dn:
+			raise exceptions.LDAPNoSuchObject()
+		try:
+			attribute_type = self.schema.attribute_types[attribute]
+		except KeyError as exc:
+			raise exceptions.LDAPUndefinedAttributeType() from exc
+		return attribute_type.match_equal(self.get(attribute_type, subtypes=True), value)
+
 class RootDSE(Object):
 	def __init__(self, schema, *args, **kwargs):
 		super().__init__(schema, DN(), *args, **kwargs)
+		self.setdefault('objectClass', ['top'])
 
 	def match_search(self, base_obj, scope, filter_obj):
-		return not base_obj and scope == ldap.SearchScope.baseObject and \
+		return not DN.from_str(base_obj) and scope == ldap.SearchScope.baseObject and \
 		       isinstance(filter_obj, ldap.FilterPresent) and \
 		       filter_obj.attribute.lower() == 'objectclass'
 
@@ -273,111 +263,21 @@ class WildcardValue:
 
 WILDCARD_VALUE = WildcardValue()
 
+class TemplateFilterResult(enum.Enum):
+	TRUE = enum.auto()
+	FALSE = enum.auto()
+	UNDEFINED = enum.auto()
+	MAYBE_TRUE = enum.auto()
+
 class ObjectTemplate(AttributeDict):
 	def __init__(self, schema, parent_dn, rdn_attribute, **attributes):
 		super().__init__(schema, **attributes)
-		self.parent_dn = parent_dn
+		self.parent_dn = DN(parent_dn)
 		self.rdn_attribute = rdn_attribute
 
-	def match_present(self, key):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		values = self[attribute_type.name]
-		if values == []:
-			return FilterResult.FALSE
-		elif WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		else:
-			return FilterResult.TRUE
-
-	def match_equal(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.equality.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		values = self.get_with_subtypes(key)
-		if WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.equality.match_equal(self.schema, attrval, assertion_value)), values))
-
-	def match_substr(self, key, inital_substring, any_substrings, final_substring):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None or attribute_type.substr is None:
-			return FilterResult.UNDEFINED
-		if inital_substring:
-			inital_substring = attribute_type.equality.syntax.decode(self.schema, inital_substring)
-			if inital_substring is None:
-				return FilterResult.UNDEFINED
-		any_substrings = [attribute_type.equality.syntax.decode(self.schema, substring) for substring in any_substrings]
-		if None in any_substrings:
-			return FilterResult.UNDEFINED
-		if final_substring:
-			final_substring = attribute_type.equality.syntax.decode(self.schema, final_substring)
-			if final_substring is None:
-				return FilterResult.UNDEFINED
-		values = self.get_with_subtypes(key)
-		if WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.substr.match_substr(self.schema, attrval, inital_substring, any_substrings, final_substring)), values))
-
-	def match_approx(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.equality is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.equality.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		values = self.get_with_subtypes(key)
-		if WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.equality.match_approx(self.schema, attrval, assertion_value)), values))
-
-	def match_greater_or_equal(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.ordering is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.ordering.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		values = self.get_with_subtypes(key)
-		if WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.ordering.match_greater_or_equal(self.schema, attrval, assertion_value)), values))
-
-	def match_less(self, key, assertion_value):
-		try:
-			attribute_type = self.schema.get_attribute_type(key)
-		except KeyError:
-			return FilterResult.UNDEFINED
-		if attribute_type.ordering is None:
-			return FilterResult.UNDEFINED
-		assertion_value = attribute_type.ordering.syntax.decode(self.schema, assertion_value)
-		if assertion_value is None:
-			return FilterResult.UNDEFINED
-		values = self.get_with_subtypes(key)
-		if WILDCARD_VALUE in values:
-			return FilterResult.MAYBE_TRUE
-		return any_3value(map(lambda attrval: match_to_filter_result(attribute_type.ordering.match_less(self.schema, attrval, assertion_value)), values))
-
-	def __extract_dn_constraints(self, basedn, scope):
+	def __match_extract_dn_constraints(self, basedn, scope):
 		if scope == ldap.SearchScope.baseObject:
-			if basedn[1:] != self.parent_dn or basedn.object_attribute != self.rdn_attribute:
+			if basedn[1:] != self.parent_dn or basedn.object_attribute.lower() != self.rdn_attribute.lower():
 				return False, AttributeDict(self.schema)
 			return True, AttributeDict(self.schema, **{self.rdn_attribute: [basedn.object_value]})
 		elif scope == ldap.SearchScope.singleLevel:
@@ -385,46 +285,157 @@ class ObjectTemplate(AttributeDict):
 		elif scope == ldap.SearchScope.wholeSubtree:
 			if self.parent_dn.in_subtree_of(basedn):
 				return True, AttributeDict(self.schema)
-			if basedn[1:] != self.parent_dn or basedn.object_attribute != self.rdn_attribute:
+			if basedn[1:] != self.parent_dn or basedn.object_attribute.lower() != self.rdn_attribute.lower():
 				return False, AttributeDict(self.schema)
 			return True, AttributeDict(self.schema, **{self.rdn_attribute: [basedn.object_value]})
 		else:
 			return False, AttributeDict(self.schema)
 
-	def match_dn(self, basedn, scope):
+	def __search_match_dn(self, basedn, scope):
 		'''Return whether objects from this template might match the provided parameters'''
-		return self.__extract_dn_constraints(basedn, scope)[0]
+		return self.__match_extract_dn_constraints(basedn, scope)[0]
 
-	def extract_dn_constraints(self, basedn, scope):
-		return self.__extract_dn_constraints(basedn, scope)[1]
+	def __extract_dn_constraints(self, basedn, scope):
+		return self.__match_extract_dn_constraints(basedn, scope)[1]
+
+	def __search_match_filter(self, filter_obj):
+		# pylint: disable=too-many-return-statements,too-many-branches,too-many-nested-blocks,too-many-statements
+		if isinstance(filter_obj, ldap.FilterAnd):
+			result = TemplateFilterResult.TRUE
+			for subfilter in filter_obj.filters:
+				subresult = self.__search_match_filter(subfilter)
+				if subresult == TemplateFilterResult.FALSE:
+					return TemplateFilterResult.FALSE
+				elif subresult == TemplateFilterResult.UNDEFINED:
+					result = TemplateFilterResult.UNDEFINED
+				elif subresult == TemplateFilterResult.MAYBE_TRUE and result == TemplateFilterResult.TRUE:
+					result = TemplateFilterResult.MAYBE_TRUE
+			return result
+		elif isinstance(filter_obj, ldap.FilterOr):
+			result = TemplateFilterResult.FALSE
+			for subfilter in filter_obj.filters:
+				subresult = self.__search_match_filter(subfilter)
+				if subresult == TemplateFilterResult.TRUE:
+					return TemplateFilterResult.TRUE
+				elif subresult == TemplateFilterResult.MAYBE_TRUE:
+					result = TemplateFilterResult.MAYBE_TRUE
+				elif subresult == TemplateFilterResult.UNDEFINED and result == TemplateFilterResult.FALSE:
+					result = TemplateFilterResult.UNDEFINED
+			return result
+		elif isinstance(filter_obj, ldap.FilterNot):
+			subresult = self.__search_match_filter(filter_obj.filter)
+			if subresult == TemplateFilterResult.TRUE:
+				return TemplateFilterResult.FALSE
+			elif subresult == TemplateFilterResult.FALSE:
+				return TemplateFilterResult.TRUE
+			else:
+				return subresult
+		elif isinstance(filter_obj, (ldap.FilterPresent,
+		                             ldap.FilterEqual,
+		                             ldap.FilterSubstrings,
+		                             ldap.FilterApproxMatch,
+		                             ldap.FilterGreaterOrEqual,
+		                             ldap.FilterLessOrEqual)):
+			try:
+				attribute_type = self.schema.attribute_types[filter_obj.attribute]
+			except KeyError:
+				return TemplateFilterResult.UNDEFINED
+			values = self.get(filter_obj.attribute, subtypes=True)
+			is_wildcard = WILDCARD_VALUE in values
+			if is_wildcard:
+				values = []
+			try:
+				if isinstance(filter_obj, ldap.FilterPresent):
+					result = values != []
+				elif isinstance(filter_obj, ldap.FilterEqual):
+					result = attribute_type.match_equal(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterSubstrings):
+					result = attribute_type.match_substr(values,
+							filter_obj.initial_substring, filter_obj.any_substrings, filter_obj.final_substring)
+				elif isinstance(filter_obj, ldap.FilterApproxMatch):
+					result = attribute_type.match_approx(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterGreaterOrEqual):
+					result = attribute_type.match_greater_or_equal(values, filter_obj.value)
+				elif isinstance(filter_obj, ldap.FilterLessOrEqual):
+					result = attribute_type.match_less_or_equal(values, filter_obj.value)
+				else:
+					return TemplateFilterResult.UNDEFINED
+				if result:
+					return TemplateFilterResult.TRUE
+			except exceptions.LDAPError:
+				return TemplateFilterResult.UNDEFINED
+			if is_wildcard:
+				return TemplateFilterResult.MAYBE_TRUE
+			return TemplateFilterResult.FALSE
+		elif isinstance(filter_obj, ldap.FilterExtensibleMatch):
+			attribute_types = []
+			matching_rule = None
+			try:
+				if filter_obj.type is not None and filter_obj.matchingRule is not None:
+					attribute_types = [self.schema.attribute_types[filter_obj.type]]
+					matching_rule = self.schema.matching_rules[filter_obj.matchingRule]
+				elif filter_obj.type is not None:
+					attribute_types = [self.schema.attribute_types[filter_obj.type]]
+				elif filter_obj.matchingRule is not None:
+					matching_rule = self.schema.matching_rules[filter_obj.matchingRule]
+					attribute_types = matching_rule.compatible_attribute_types
+			except KeyError:
+				pass
+			result = TemplateFilterResult.FALSE
+			for attribute_type in attribute_types:
+				values = self.get(attribute_type.oid, subtypes=True)
+				if filter_obj.dnAttributes:
+					for rdn in self.parent_dn:
+						for assertion in rdn:
+							if assertion.attribute.lower() == attribute_type.ref.lower():
+								values.append(assertion.value)
+				is_wildcard = WILDCARD_VALUE in values
+				if is_wildcard:
+					values = []
+				is_undefined = False
+				try:
+					if attribute_type.match_extensible(values, filter_obj.matchValue, matching_rule):
+						return TemplateFilterResult.TRUE
+				except exceptions.LDAPError:
+					is_undefined = True
+				if is_undefined and result == TemplateFilterResult.FALSE:
+					result = TemplateFilterResult.UNDEFINED
+				elif not is_undefined and is_wildcard:
+					result = TemplateFilterResult.MAYBE_TRUE
+			return result
+		else:
+			return TemplateFilterResult.UNDEFINED
 
-	def extract_filter_constraints(self, filter_obj):
+	def __extract_filter_constraints(self, filter_obj):
 		if isinstance(filter_obj, ldap.FilterEqual):
 			try:
-				attribute_type = self.schema.get_attribute_type(filter_obj.attribute)
+				attribute_type = self.schema.attribute_types[filter_obj.attribute]
 			except KeyError:
 				return AttributeDict(self.schema)
 			if attribute_type.equality is None:
 				return AttributeDict(self.schema)
-			assertion_value = attribute_type.equality.syntax.decode(self.schema, filter_obj.value)
+			assertion_value = attribute_type.equality.syntax.decode(filter_obj.value)
 			if assertion_value is None:
 				return AttributeDict(self.schema)
 			return AttributeDict(self.schema, **{filter_obj.attribute: [assertion_value]})
 		if isinstance(filter_obj, ldap.FilterAnd):
 			result = AttributeDict(self.schema)
 			for subfilter in filter_obj.filters:
-				for name, values in self.extract_filter_constraints(subfilter).items():
+				for name, values in self.__extract_filter_constraints(subfilter).items():
 					result[name] += values
 			return result
 		return AttributeDict(self.schema)
 
 	def match_search(self, base_obj, scope, filter_obj):
 		'''Return whether objects based on this template might match the search parameters'''
-		return self.match_dn(DN.from_str(base_obj), scope) and self.match_filter(filter_obj) in (FilterResult.TRUE, FilterResult.MAYBE_TRUE)
+		print(base_obj, scope, filter_obj, self.__search_match_filter(filter_obj))
+		return self.__search_match_dn(DN.from_str(base_obj), scope) and \
+		       self.__search_match_filter(filter_obj) in (TemplateFilterResult.TRUE,
+		                                                  TemplateFilterResult.MAYBE_TRUE)
 
 	def extract_search_constraints(self, base_obj, scope, filter_obj):
-		constraints = self.extract_filter_constraints(filter_obj)
-		for key, values in self.extract_dn_constraints(DN.from_str(base_obj), scope).items():
+		constraints = self.__extract_filter_constraints(filter_obj)
+		for key, values in self.__extract_dn_constraints(DN.from_str(base_obj), scope).items():
 			constraints[key] += values
 		return constraints
 
@@ -446,10 +457,11 @@ class SubschemaSubentry(Object):
 		self['subschemaSubentry'] = [self.dn]
 		self['structuralObjectClass'] = ['subtree']
 		self['objectClass'] = ['top', 'subtree', 'subschema']
-		self['objectClasses'] = schema.object_classes
-		self['ldapSyntaxes'] = schema.syntaxes
-		self['matchingRules'] = schema.matching_rules
-		self['attributeTypes'] = schema.attribute_types
+		self['objectClasses'] = schema.object_class_definitions
+		self['ldapSyntaxes'] = schema.syntax_definitions
+		self['matchingRules'] = schema.matching_rule_definitions
+		self['attributeTypes'] = schema.attribute_type_definitions
+		self['matchingRuleUse'] = schema.matching_rule_use_definitions
 		# pylint: disable=invalid-name
 		self.AttributeDict = lambda **attributes: AttributeDict(schema, **attributes)
 		self.Object = lambda *args, **attributes: Object(schema, *args, subschemaSubentry=[self.dn], **attributes)
diff --git a/ldapserver/rfc4518_stringprep.py b/ldapserver/rfc4518_stringprep.py
index d4d07b02d9def7ff732ea8e97443cac3eb2dc1d8..6e4c93c2a06fff0346de78518c0d06fd5c3658ea 100644
--- a/ldapserver/rfc4518_stringprep.py
+++ b/ldapserver/rfc4518_stringprep.py
@@ -208,9 +208,9 @@ def prepare_map(value, matching_type=MatchingType.EXACT_STRING):
 	# The output is the mapped string.
 	new_value = ''
 	for char in value:
-		if char in MAPPED_TO_NOTHING:
+		if ord(char) in MAPPED_TO_NOTHING:
 			continue
-		if char in MAPPED_TO_SPACE:
+		if ord(char) in MAPPED_TO_SPACE:
 			char = ' '
 		# No idea what "stored prefix string matching" is supposed to be
 		if matching_type in (MatchingType.CASE_IGNORE_STRING,
diff --git a/ldapserver/schema/__init__.py b/ldapserver/schema/__init__.py
index 0e4298dd7fa8beddcbdee95426b25d1408d14798..8dda82c744ee00e9579c2cc68bda7406bf141b40 100644
--- a/ldapserver/schema/__init__.py
+++ b/ldapserver/schema/__init__.py
@@ -1,14 +1,256 @@
-from .types import *
-from . import rfc4517, rfc4512, rfc4519, rfc4524, rfc3112, rfc2307bis, rfc2079, rfc2252, rfc2798, rfc4523, rfc1274
+from .types import Schema
+from .definitions import *
+from . import syntaxes, matching_rules
 
-# Core LDAP Schema
-RFC4519_SUBSCHEMA = Schema( rfc4519.object_classes.ALL, rfc4519.attribute_types.ALL, rfc4519.matching_rules.ALL, rfc4519.syntaxes.ALL)
+RFC4512_ATTRIBUTE_TYPES = [
+	"( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )",
+	"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+	"( 2.5.18.3 NAME 'creatorsName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.18.1 NAME 'createTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.18.4 NAME 'modifiersName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.18.2 NAME 'modifyTimestamp' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.21.9 NAME 'structuralObjectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation ) ",
+	"( 2.5.21.10 NAME 'governingStructureRule' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.18.10 NAME 'subschemaSubentry' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE NO-USER-MODIFICATION USAGE directoryOperation )",
+	"( 2.5.21.6 NAME 'objectClasses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )",
+	"( 2.5.21.5 NAME 'attributeTypes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.3 USAGE directoryOperation )",
+	"( 2.5.21.4 NAME 'matchingRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.30 USAGE directoryOperation )",
+	"( 2.5.21.8 NAME 'matchingRuleUse' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.31 USAGE directoryOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.16 NAME 'ldapSyntaxes' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.54 USAGE directoryOperation )",
+	"( 2.5.21.2 NAME 'dITContentRules' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.16 USAGE directoryOperation )",
+	"( 2.5.21.1 NAME 'dITStructureRules' EQUALITY integerFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.17 USAGE directoryOperation )",
+	"( 2.5.21.7 NAME 'nameForms' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.35 USAGE directoryOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.6 NAME 'altServer' SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.5 NAME 'namingContexts' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.13 NAME 'supportedControl' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.7 NAME 'supportedExtension' SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.4203.1.3.5 NAME 'supportedFeatures' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.15 NAME 'supportedLDAPVersion' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.1466.101.120.14 NAME 'supportedSASLMechanisms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 USAGE dSAOperation )",
+]
+RFC4512_OBJECT_CLASSES = [
+	"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+	"( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )",
+	"( 1.3.6.1.4.1.1466.101.120.111 NAME 'extensibleObject' SUP top AUXILIARY )",
+	"( 2.5.20.1 NAME 'subschema' AUXILIARY MAY ( dITStructureRules $ nameForms $ ditContentRules $ objectClasses $ attributeTypes $ matchingRules $ matchingRuleUse ) )",
+]
+CORE_SCHEMA = RFC4512_SCHEMA = Schema(syntax_definitions=syntaxes.ALL, matching_rule_definitions=matching_rules.ALL, attribute_type_definitions=RFC4512_ATTRIBUTE_TYPES, object_class_definitions=RFC4512_OBJECT_CLASSES)
 
-# COSINE LDAP/X.500 Schema
-RFC4524_SUBSCHEMA = Schema(rfc4524.object_classes.ALL, rfc4524.attribute_types.ALL, rfc4524.matching_rules.ALL, rfc4524.syntaxes.ALL)
+RFC4519_ATTRIBUTE_TYPES = [
+	"( 2.5.4.15 NAME 'businessCategory' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.6 NAME ( 'c' 'countryName' ) SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE )",
+	"( 2.5.4.3 NAME ( 'cn' 'commonName' ) SUP name )",
+	"( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainComponent' ) EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )",
+	"( 2.5.4.13 NAME 'description' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.27 NAME 'destinationIndicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )",
+	"( 2.5.4.49 NAME 'distinguishedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )",
+	"( 2.5.4.46 NAME 'dnQualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )",
+	"( 2.5.4.47 NAME 'enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )",
+	"( 2.5.4.23 NAME 'facsimileTelephoneNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 )",
+	"( 2.5.4.44 NAME 'generationQualifier' SUP name )",
+	"( 2.5.4.42 NAME ( 'givenName' 'gn' ) SUP name )",
+	"( 2.5.4.51 NAME 'houseIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.43 NAME 'initials' SUP name )",
+	"( 2.5.4.25 NAME 'internationalISDNNumber' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )",
+	"( 2.5.4.7 NAME ( 'l' 'localityName' ) SUP name )",
+	"( 2.5.4.31 NAME 'member' SUP distinguishedName )",
+	"( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.10 NAME ( 'o' 'organizationName' ) SUP name )",
+	"( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) SUP name )",
+	"( 2.5.4.32 NAME 'owner' SUP distinguishedName )",
+	"( 2.5.4.19 NAME 'physicalDeliveryOfficeName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.16 NAME 'postalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )",
+	"( 2.5.4.17 NAME 'postalCode' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.18 NAME 'postOfficeBox' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.28 NAME 'preferredDeliveryMethod' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE )",
+	"( 2.5.4.26 NAME 'registeredAddress' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )",
+	"( 2.5.4.33 NAME 'roleOccupant' SUP distinguishedName )",
+	"( 2.5.4.14 NAME 'searchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 )",
+	"( 2.5.4.34 NAME 'seeAlso' SUP distinguishedName )",
+	"( 2.5.4.5 NAME 'serialNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )",
+	"( 2.5.4.4 NAME ( 'sn' 'surname' ) SUP name )",
+	"( 2.5.4.8 NAME 'st' SUP name )",
+	"( 2.5.4.9 NAME 'street' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.20 NAME 'telephoneNumber' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )",
+	"( 2.5.4.22 NAME 'teletexTerminalIdentifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )",
+	"( 2.5.4.21 NAME 'telexNumber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )",
+	"( 2.5.4.12 NAME 'title' SUP name )",
+	"( 0.9.2342.19200300.100.1.1 NAME ( 'uid' 'userid' ) EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.5.4.50 NAME 'uniqueMember' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )",
+	"( 2.5.4.35 NAME 'userPassword' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+	"( 2.5.4.24 NAME 'x121Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36 )",
+	"( 2.5.4.45 NAME 'x500UniqueIdentifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 )",
+]
+RFC4519_OBJECT_CLASSES = [
+	"( 2.5.6.11 NAME 'applicationProcess' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) )",
+	"( 2.5.6.2 NAME 'country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) )",
+	"( 1.3.6.1.4.1.1466.344 NAME 'dcObject' SUP top AUXILIARY MUST dc )",
+	"( 2.5.6.14 NAME 'device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) )",
+	"( 2.5.6.9 NAME 'groupOfNames' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )",
+	"( 2.5.6.17 NAME 'groupOfUniqueNames' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )",
+	"( 2.5.6.3 NAME 'locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) )",
+	"( 2.5.6.4 NAME 'organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )",
+	"( 2.5.6.7 NAME 'organizationalPerson' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) )",
+	"( 2.5.6.8 NAME 'organizationalRole' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) )",
+	"( 2.5.6.5 NAME 'organizationalUnit' SUP top STRUCTURAL MUST ou MAY ( businessCategory $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationalISDNNumber $ l $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ searchGuide $ seeAlso $ st $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ userPassword $ x121Address ) )",
+	"( 2.5.6.6 NAME 'person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) )",
+	"( 2.5.6.10 NAME 'residentialPerson' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationalISDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) )",
+	"( 1.3.6.1.1.3.1 NAME 'uidObject' SUP top AUXILIARY MUST uid )",
+]
+RFC4519_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC4519_ATTRIBUTE_TYPES, object_class_definitions=RFC4519_OBJECT_CLASSES)
 
-# inetOrgPerson Schema
-RFC2798_SUBSCHEMA = Schema(rfc2798.object_classes.ALL, rfc2798.attribute_types.ALL, rfc2798.matching_rules.ALL, rfc2798.syntaxes.ALL)
+RFC4523_ATTRIBUTE_TYPES = [
+	"( 2.5.4.36 NAME 'userCertificate' DESC 'X.509 user certificate' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )",
+	"( 2.5.4.37 NAME 'cACertificate' DESC 'X.509 CA certificate' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 )",
+	"( 2.5.4.40 NAME 'crossCertificatePair' DESC 'X.509 cross certificate pair' EQUALITY certificatePairExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 )",
+	"( 2.5.4.39 NAME 'certificateRevocationList' DESC 'X.509 certificate revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )",
+	"( 2.5.4.38 NAME 'authorityRevocationList' DESC 'X.509 authority revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )",
+	"( 2.5.4.53 NAME 'deltaRevocationList' DESC 'X.509 delta revocation list' EQUALITY certificateListExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )",
+	"( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'X.509 supported algorithms' EQUALITY algorithmIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 )",
+]
+RFC4523_OBJECT_CLASSES = [
+	"( 2.5.6.21 NAME 'pkiUser' DESC 'X.509 PKI User' SUP top AUXILIARY MAY userCertificate )",
+	"( 2.5.6.22 NAME 'pkiCA' DESC 'X.509 PKI Certificate Authority' SUP top AUXILIARY MAY ( cACertificate $ certificateRevocationList $ authorityRevocationList $ crossCertificatePair ) )",
+	"( 2.5.6.19 NAME 'cRLDistributionPoint' DESC 'X.509 CRL distribution point' SUP top STRUCTURAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) )",
+	"( 2.5.6.23 NAME 'deltaCRL' DESC 'X.509 delta CRL' SUP top AUXILIARY MAY deltaRevocationList )",
+	"( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'X.521 strong authentication user' SUP top AUXILIARY MUST userCertificate )",
+	"( 2.5.6.18 NAME 'userSecurityInformation' DESC 'X.521 user security information' SUP top AUXILIARY MAY ( supportedAlgorithms ) )",
+	"( 2.5.6.16 NAME 'certificationAuthority' DESC 'X.509 certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair )",
+	"( 2.5.6.16.2 NAME 'certificationAuthority-V2' DESC 'X.509 certificate authority, version 2' SUP certificationAuthority AUXILIARY MAY deltaRevocationList )",
+]
+RFC4523_SCHEMA = RFC4519_SCHEMA.extend(attribute_type_definitions=RFC4523_ATTRIBUTE_TYPES, object_class_definitions=RFC4523_OBJECT_CLASSES)
 
-# Extended RFC2307 (NIS) Schema
-RFC2307BIS_SUBSCHEMA = Schema(rfc2307bis.object_classes.ALL, rfc2307bis.attribute_types.ALL, rfc2307bis.matching_rules.ALL, rfc2307bis.syntaxes.ALL)
+RFC4524_ATTRIBUTE_TYPES = [
+	"( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )",
+	"( 0.9.2342.19200300.100.1.38 NAME 'associatedName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )",
+	"( 0.9.2342.19200300.100.1.48 NAME 'buildingName' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.43 NAME 'co' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )",
+	"( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.5 NAME 'drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.20 NAME 'homePhone' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )",
+	"( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )",
+	"( 0.9.2342.19200300.100.1.9 NAME 'host' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.4 NAME 'info' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} )",
+	"( 0.9.2342.19200300.100.1.3 NAME 'mail' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )",
+	"( 0.9.2342.19200300.100.1.10 NAME 'manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )",
+	"( 0.9.2342.19200300.100.1.41 NAME 'mobile' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )",
+	"( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.42 NAME 'pager' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 )",
+	"( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.21 NAME 'secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )",
+	"( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+	"( 0.9.2342.19200300.100.1.8 NAME 'userClass' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )",
+]
+RFC4524_OBJECT_CLASSES = [
+	"( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST uid MAY ( description $ seeAlso $ l $ o $ ou $ host ) )",
+	"( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( cn $ description $ seeAlso $ l $ o $ ou $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) )",
+	"( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST cn MAY ( description $ l $ o $ ou $ seeAlso $ telephonenumber ) )",
+	"( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST dc MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description $ o $ associatedName ) )",
+	"( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' SUP top AUXILIARY MUST associatedDomain )",
+	"( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST co )",
+	"( 0.9.2342.19200300.100.4.14 NAME 'rFC822localPart' SUP domain STRUCTURAL MAY ( cn $ description $ destinationIndicator $ facsimileTelephoneNumber $ internationaliSDNNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ preferredDeliveryMethod $ registeredAddress $ seeAlso $ sn $ street $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ x121Address ) )",
+	"( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST cn MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) )",
+	"( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' SUP top AUXILIARY MUST userPassword )",
+]
+COSINE_SCHEMA = RFC4524_SCHEMA = RFC4519_SCHEMA.extend(attribute_type_definitions=RFC4524_ATTRIBUTE_TYPES, object_class_definitions=RFC4524_OBJECT_CLASSES)
+
+RFC3112_ATTRIBUTE_TYPES = [
+	"( 1.3.6.1.4.1.4203.1.3.3 NAME 'supportedAuthPasswordSchemes' DESC 'supported password storage schemes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} USAGE dSAOperation )",
+	"( 1.3.6.1.4.1.4203.1.3.4 NAME 'authPassword' DESC 'password authentication information' EQUALITY 1.3.6.1.4.1.4203.1.2.2 SYNTAX 1.3.6.1.4.1.4203.1.1.2 )",
+]
+RFC3112_OBJECT_CLASSES = [
+	"( 1.3.6.1.4.1.4203.1.4.7 NAME 'authPasswordObject' DESC 'authentication password mix in class' AUXILIARY MAY authPassword )",
+]
+RFC3112_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC3112_ATTRIBUTE_TYPES, object_class_definitions=RFC3112_OBJECT_CLASSES)
+
+RFC2079_ATTRIBUTE_TYPES = [
+	"( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+]
+RFC2079_OBJECT_CLASSES = [
+	"( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI )",
+]
+RFC2079_SCHEMA = CORE_SCHEMA.extend(attribute_type_definitions=RFC2079_ATTRIBUTE_TYPES, object_class_definitions=RFC2079_OBJECT_CLASSES)
+
+RFC2798_ATTRIBUTE_TYPES = [
+	# Originally from RFC1274, but updated and used by RFC2798
+	"( 0.9.2342.19200300.100.1.55 NAME 'audio' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40{250000} )",
+	# Also from RFC1274 and updated by RFC2798, but lacks SYNTAX, so "Fax" is added here
+	"( 0.9.2342.19200300.100.1.7 NAME 'photo' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23 )",
+
+	"( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC 'identifies a department within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'preferred name of a person to be used when displaying entries' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )",
+	"( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DESC 'preferred written or spoken language for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate' DESC 'signed message used to support S/MIME' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )",
+	"( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'PKCS #12 PFX PDU for exchange of personal identity information' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 )",
+]
+RFC2798_OBJECT_CLASSES = [
+	"( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) )",
+]
+INETORG_SCHMEA = RFC2798_SCHEMA = (RFC4524_SCHEMA|RFC2079_SCHEMA|RFC4523_SCHEMA).extend(attribute_type_definitions=RFC2798_ATTRIBUTE_TYPES, object_class_definitions=RFC2798_OBJECT_CLASSES)
+
+RFC2307BIS_ATTRIBUTE_TYPES = [
+	"( 1.3.6.1.1.1.1.0 NAME 'uidNumber' DESC 'An integer uniquely identifying a user in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.1 NAME 'gidNumber' DESC 'An integer uniquely identifying a group in an administrative domain' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' DESC 'Service port number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' DESC 'Service protocol name' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+	"( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' DESC 'IP protocol number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' DESC 'ONC RPC number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC2373' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )",
+	"( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network omitting leading zeros, eg. 192.168' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask omitting leading zeros, eg. 255.255.255.0' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )",
+	"( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )",
+	"( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )",
+	"( 1.3.6.1.1.1.1.26 NAME 'nisMapName' DESC 'Name of a generic NIS map' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{64} )",
+	"( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' DESC 'A generic NIS entry' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1024} SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey' DESC 'NIS public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey' DESC 'NIS secret key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.30 NAME 'nisDomain' DESC 'NIS domain' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )",
+	"( 1.3.6.1.1.1.1.31 NAME 'automountMapName' DESC 'automount Map Name' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.32 NAME 'automountKey' DESC 'Automount Key value' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+	"( 1.3.6.1.1.1.1.33 NAME 'automountInformation' DESC 'Automount information' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )",
+]
+RFC2307BIS_OBJECT_CLASSES = [
+	"( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( authPassword $ userPassword $ loginShell $ gecos $ description ) )",
+	"( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( authPassword $ userPassword $ description $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag ) )",
+	"( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top AUXILIARY MUST gidNumber MAY ( authPassword $ userPassword $ memberUid $ description ) )",
+	"( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an Internet Protocol service.  Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the service\\27s canonical name' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description )",
+	"( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocol canonical name' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber ) MAY description )",
+	"( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding.  This class maps an ONC RPC number to a name.  The distinguished value of the cn attribute denotes the RPC service canonical name' SUP top STRUCTURAL MUST ( cn $ oncRpcNumber ) MAY description )",
+	"( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the host\\27s canonical name. Device SHOULD be used as a structural class' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( authPassword $ userPassword $ l $ description $ manager ) )",
+	"( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of a network. The distinguished value of the cn attribute denotes the network canonical name' SUP top STRUCTURAL MUST ipNetworkNumber MAY ( cn $ ipNetmaskNumber $ l $ description $ manager ) )",
+	"( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction of a netgroup. May refer to other netgroups' SUP top STRUCTURAL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )",
+	"( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstraction of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description )",
+	"( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) )",
+	"( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device with a MAC address; device SHOULD be used as a structural class' SUP top AUXILIARY MAY macAddress )",
+	"( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A device with boot parameters; device SHOULD be used as a structural class' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) )",
+	"( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' DESC 'An object with a public and secret key' SUP top AUXILIARY MUST ( cn $ nisPublicKey $ nisSecretKey ) MAY ( uidNumber $ description ) )",
+	"( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' DESC 'Associates a NIS domain with a naming context' SUP top AUXILIARY MUST nisDomain )",
+	"( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTURAL MUST ( automountMapName ) MAY description )",
+	"( 1.3.6.1.1.1.2.17 NAME 'automount' DESC 'Automount information' SUP top STRUCTURAL MUST ( automountKey $ automountInformation ) MAY description )",
+	"( 1.3.6.1.1.1.2.18 NAME 'groupOfMembers' DESC 'A group with members (DNs)' SUP top STRUCTURAL MUST cn MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description $ member ) )",
+]
+RFC2307BIS_SCHEMA = (RFC4524_SCHEMA|RFC3112_SCHEMA).extend(attribute_type_definitions=RFC2307BIS_ATTRIBUTE_TYPES, object_class_definitions=RFC2307BIS_OBJECT_CLASSES)
diff --git a/ldapserver/schema/definitions.py b/ldapserver/schema/definitions.py
new file mode 100644
index 0000000000000000000000000000000000000000..c52c745c2bb7c913afd17a8931e588dc41dc5e20
--- /dev/null
+++ b/ldapserver/schema/definitions.py
@@ -0,0 +1,524 @@
+import enum
+import re
+
+from .. import exceptions
+
+__all__ = [
+	'SyntaxDefinition',
+	'MatchingRuleKind',
+	'MatchingRuleDefinition',
+	'MatchingRuleUseDefinition',
+	'AttributeTypeUsage',
+	'AttributeTypeDefinition',
+	'ObjectClassKind',
+	'ObjectClassDefinition',
+]
+
+def escape(string):
+	return string.replace('\\', '\\5C').replace('\'', '\\27')
+
+def qdescr_to_token(descr):
+	return '\''+descr+'\''
+
+def qdescrs_to_tokens(descrs):
+	if len(descrs) == 1:
+		return [qdescr_to_token(descrs[0])]
+	return ['('] + [qdescr_to_token(descr) for descr in descrs] + [')']
+
+def oids_to_tokens(oids):
+	if len(oids) == 1:
+		return [oids[0]]
+	tokens = ['(', oids[0]]
+	for oid in oids[1:]:
+		tokens += ['$', oid]
+	return tokens + [')']
+
+def qdstring_to_token(qdstring):
+	return '\''+escape(qdstring)+'\''
+
+def qdstrings_to_tokens(qdstrings):
+	if len(qdstrings) == 1:
+		return [qdstring_to_token(qdstrings[0])]
+	return ['('] + [qdstring_to_token(qdstring) for qdstring in qdstrings] + [')']
+
+def extensions_to_tokens(extensions):
+	tokens = []
+	if not extensions:
+		return []
+	for key, values in extensions.items():
+		if not key.startswith('X-'):
+			raise ValueError('Extention names must start with "X-"')
+		tokens += [key] + qdstrings_to_tokens(values)
+	return tokens
+
+def tokenize(string):
+	tokens = []
+	offset = 0
+	while string:
+		match = re.match(r" *([()$]|[A-Za-z0-9.{}-]+|'[^']*') *", string)
+		if not match:
+			string_abbrev = string[:20] + '...'
+			raise ValueError(f'Unrecognized token at offset {offset}: "{string_abbrev}"')
+		tokens.append(match.groups()[0])
+		string = string[match.end():]
+		offset += match.end()
+	return tokens
+
+def pop_token(tokens):
+	if not tokens:
+		raise ValueError('Unexpected end of input')
+	return tokens.pop(0)
+
+def check_token(tokens, expected_token):
+	if not tokens or tokens[0] != expected_token:
+		return False
+	tokens.pop(0)
+	return True
+
+def parse_token(tokens, expected_token):
+	token = pop_token(tokens)
+	if token != expected_token:
+		raise ValueError(f'Expected "{expected_token}" but got "{token}"')
+	return token
+
+def parse_numericoid(tokens):
+	token = pop_token(tokens)
+	if not re.fullmatch(r"([0-9]|[1-9][0-9]*)(\.([0-9]|[1-9][0-9]*))*", token):
+		raise ValueError(f'Invalid numeric OID "{token}"')
+	return token
+
+def parse_qdescr(tokens):
+	token = pop_token(tokens)
+	match = re.fullmatch(r"'([A-Za-z][A-Za-z0-9-]*)'", token)
+	if not match:
+		raise ValueError(f'Invalid quoted descriptor "{token}"')
+	return match.groups()[0]
+
+def parse_qdescrs(tokens):
+	if check_token(tokens, '('):
+		result = []
+		while not check_token(tokens, ')'):
+			result.append(parse_qdescr(tokens))
+		return result
+	return [parse_qdescr(tokens)]
+
+def parse_qdstring(tokens):
+	token = pop_token(tokens)
+	match = re.fullmatch(r"'(([^'\\]|\\27|\\5C|\\5c)+)'", token)
+	if not match:
+		raise ValueError(f'Invalid quoted string "{token}"')
+	return match.groups()[0].replace('\\27', '\'').replace('\\5C', '\\').replace('\\5c', '\\')
+
+def parse_qdstrings(tokens):
+	if check_token(tokens, '('):
+		result = []
+		while not check_token(tokens, ')'):
+			result.append(parse_qdstring(tokens))
+		return result
+	return [parse_qdstring(tokens)]
+
+def parse_oid(tokens):
+	token = pop_token(tokens)
+	if not re.fullmatch(r'([0-9]|[1-9][0-9]*)(\.([0-9]|[1-9][0-9]*))*|[A-Za-z][A-Za-z0-9-]*', token):
+		raise ValueError(f'Invalid OID "{token}"')
+	return token
+
+def parse_oids(tokens):
+	if check_token(tokens, '('):
+		result = [parse_oid(tokens)]
+		while not check_token(tokens, ')'):
+			parse_token(tokens, '$')
+			result.append(parse_oid(tokens))
+		return result
+	return [parse_oid(tokens)]
+
+def parse_noidlen(tokens):
+	token = pop_token(tokens)
+	match = re.fullmatch(r"(([0-9]|[1-9][0-9]*)(\.([0-9]|[1-9][0-9]*))*)(|\{([0-9]|[1-9][0-9]*)\})", token)
+	if not match:
+		raise ValueError(f'Invalid numeric OID with optional length "{token}"')
+	noid, _, _, _, _, length = match.groups()
+	if length is not None:
+		length = int(length)
+	return noid, length
+
+def parse_extensions(tokens):
+	results = {}
+	while tokens and re.fullmatch(r"X-[A-Z_-]+", tokens[0]):
+		name = tokens.pop(0)
+		values = parse_qdstrings(tokens)
+		results[name] = values
+	return results
+
+class SyntaxDefinition:
+	def __init__(self, oid, desc='', extensions=None, extra_compatability_tags=None):
+		self.oid = oid
+		self.desc = desc
+		self.extensions = extensions or {}
+		self.compatability_tags = {oid} | set(extra_compatability_tags or tuple())
+
+	def __str__(self):
+		tokens = ['(', self.oid]
+		if self.desc:
+			tokens += ['DESC', qdstring_to_token(self.desc)]
+		tokens += extensions_to_tokens(self.extensions) + [')']
+		return ' '.join(tokens)
+
+	@property
+	def first_component_oid(self):
+		return self.oid
+
+	def encode(self, schema, value):
+		'''Encode native value to its LDAP-specific encoding
+
+		:param schema: Schema of the object in whose context encoding takes place
+		:type schema: Schema
+		:param value: native value (depends on syntax)
+		:type value: any
+
+		:returns: LDAP-specific encoding of the value
+		:rtype: bytes'''
+		raise NotImplementedError()
+
+	def decode(self, schema, raw_value):
+		'''Decode LDAP-specific encoding of a value to a native value
+
+		:param schema: Schema of the object in whose context decoding takes place
+		:type schema: Schema
+		:param raw_value: LDAP-specific encoding of the value
+		:type raw_value: bytes
+
+		:returns: native value (depends on syntax)
+		:rtype: any
+		:raises exceptions.LDAPError: if raw_value is invalid'''
+		raise exceptions.LDAPInvalidAttributeSyntax()
+
+class MatchingRuleKind(enum.Enum):
+	EQUALITY = enum.auto()
+	ORDERING = enum.auto()
+	SUBSTR = enum.auto()
+
+class MatchingRuleDefinition:
+	# pylint: disable=too-many-arguments
+	def __init__(self, oid, name=None, desc='', obsolete=False, syntax=None, extensions=None, compatability_tag=None, kind=None):
+		if not syntax:
+			raise ValueError('syntax must be specified')
+		if not kind:
+			raise ValueError('kind must be specified')
+		self.oid = oid
+		self.syntax = syntax
+		self.name = name or []
+		self.desc = desc
+		self.obsolete = obsolete
+		self.extensions = extensions or {}
+		self.compatability_tag = compatability_tag or syntax
+		self.kind = kind
+
+	def __str__(self):
+		tokens = ['(', self.oid]
+		if self.name:
+			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
+		if self.desc:
+			tokens += ['DESC', qdstring_to_token(self.desc)]
+		if self.obsolete:
+			tokens += ['OBSOLETE']
+		tokens += ['SYNTAX', self.syntax]
+		tokens += extensions_to_tokens(self.extensions) + [')']
+		return ' '.join(tokens)
+
+	@property
+	def first_component_oid(self):
+		return self.oid
+
+	def match_equal(self, schema, attribute_values, assertion_value):
+		'''Return whether any attribute value equals assertion value
+
+		Only available for EQUALITY matching rules.
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		raise exceptions.LDAPInappropriateMatching()
+
+	def match_approx(self, schema, attribute_values, assertion_value):
+		'''Return whether any attribute value approximatly equals assertion value
+
+		Only available for EQUALITY matching rules.
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		return self.match_equal(schema, attribute_values, assertion_value)
+
+	def match_less(self, schema, attribute_values, assertion_value):
+		'''Return whether any attribute value is less than assertion value
+
+		Only available for ORDERING matching rules.
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		raise exceptions.LDAPInappropriateMatching()
+
+	def match_greater_or_equal(self, schema, attribute_values, assertion_value):
+		'''Return whether any attribute value is greater than or equal to assertion value
+
+		Only available for ORDERING matching rules.
+
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		raise exceptions.LDAPInappropriateMatching()
+
+	def match_substr(self, schema, attribute_values, inital_substring, any_substrings, final_substring):
+		'''Return whether any attribute value matches a substring assertion
+
+		Only available for SUBSTR matching rules.
+
+		The type of `inital_substring`, `any_substrings` and `final_substring`
+		depends on the syntax of the attribute's equality matching rule!
+
+		:param schema: Schema of the object whose attribute values are matched
+		:type schema: Schema
+		:param attribute_values: Attribute values (type according to attribute's syntax)
+		:type attribute_values: List of any
+		:param inital_substring: Substring to match the beginning (optional)
+		:type inital_substring: any
+		:param any_substrings: List of substrings to match between initial and final in order
+		:type any_substrings: list of any
+		:param final_substring: Substring to match the end (optional)
+		:type final_substring: any
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		raise exceptions.LDAPInappropriateMatching()
+
+class MatchingRuleUseDefinition:
+	def __init__(self, oid, name=None, desc='', obsolete=False, applies=None, extensions=None):
+		self.oid = oid
+		self.name = name or []
+		self.desc = desc
+		self.obsolete = obsolete
+		self.applies = applies or []
+		self.extensions = extensions or {}
+
+	def __str__(self):
+		tokens = ['(', self.oid]
+		if self.name:
+			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
+		if self.desc:
+			tokens += ['DESC', qdstring_to_token(self.desc)]
+		if self.obsolete:
+			tokens += ['OBSOLETE']
+		tokens += ['APPLIES'] + oids_to_tokens(self.applies)
+		tokens += extensions_to_tokens(self.extensions) + [')']
+		return ' '.join(tokens)
+
+	@property
+	def first_component_oid(self):
+		return self.oid
+
+
+class AttributeTypeUsage(enum.Enum):
+	'''Values for usage argument of `AttributeTypeUsage`'''
+	# pylint: disable=invalid-name
+	# user
+	userApplications = enum.auto()
+	# directory operational
+	directoryOperation = enum.auto()
+	# DSA-shared operational
+	distributedOperation = enum.auto()
+	# DSA-specific operational
+	dSAOperation = enum.auto()
+
+class AttributeTypeDefinition:
+	# pylint: disable=too-many-arguments,too-many-instance-attributes,too-many-locals
+	def __init__(self, oid, name=None, desc='', obsolete=False, sup=None,
+	             equality=None, ordering=None, substr=None, syntax=None,
+               syntax_len=None, single_value=False, collective=False,
+		           no_user_modification=False,
+	             usage=AttributeTypeUsage.userApplications, extensions=None):
+		if not sup and not syntax:
+			raise ValueError('Either SUP or SYNTAX must be specified')
+		self.oid = oid
+		self.name = name or []
+		self.desc = desc
+		self.obsolete = obsolete
+		self.sup = sup
+		self.equality = equality
+		self.ordering = ordering
+		self.substr = substr
+		self.syntax = syntax
+		self.syntax_len = syntax_len
+		self.single_value = single_value
+		self.collective = collective
+		self.no_user_modification = no_user_modification
+		self.usage = usage
+		self.extensions = extensions or {}
+
+	def __str__(self):
+		tokens = ['(', self.oid]
+		if self.name:
+			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
+		if self.desc:
+			tokens += ['DESC', qdstring_to_token(self.desc)]
+		if self.obsolete:
+			tokens += ['OBSOLETE']
+		if self.sup:
+			tokens += ['SUP', self.sup]
+		if self.equality:
+			tokens += ['EQUALITY', self.equality]
+		if self.ordering:
+			tokens += ['ORDERING', self.ordering]
+		if self.substr:
+			tokens += ['SUBSTR', self.substr]
+		if self.syntax:
+			if self.syntax_len is None:
+				tokens += ['SYNTAX', self.syntax]
+			else:
+				tokens += ['SYNTAX', self.syntax+'{'+str(self.syntax_len)+'}']
+		if self.single_value:
+			tokens += ['SINGLE-VALUE']
+		if self.collective:
+			tokens += ['COLLECTIVE']
+		if self.no_user_modification:
+			tokens += ['NO-USER-MODIFICATION']
+		if self.usage != AttributeTypeUsage.userApplications:
+			tokens += ['USAGE', self.usage.name]
+		tokens += extensions_to_tokens(self.extensions) + [')']
+		return ' '.join(tokens)
+
+	@classmethod
+	def from_str(cls, string):
+		tokens = tokenize(string)
+		parse_token(tokens, '(')
+		oid = parse_numericoid(tokens)
+		name = []
+		if check_token(tokens, 'NAME'):
+			name = parse_qdescrs(tokens)
+		desc = ''
+		if check_token(tokens, 'DESC'):
+			desc = parse_qdstring(tokens)
+		obsolete = check_token(tokens, 'OBSOLETE')
+		sup = None
+		if check_token(tokens, 'SUP'):
+			sup = parse_oid(tokens)
+		equality = None
+		if check_token(tokens, 'EQUALITY'):
+			equality = parse_oid(tokens)
+		ordering = None
+		if check_token(tokens, 'ORDERING'):
+			ordering = parse_oid(tokens)
+		substr = None
+		if check_token(tokens, 'SUBSTR'):
+			substr = parse_oid(tokens)
+		syntax, syntax_len = None, None
+		if check_token(tokens, 'SYNTAX'):
+			syntax, syntax_len = parse_noidlen(tokens)
+		single_value = check_token(tokens, 'SINGLE-VALUE')
+		collective = check_token(tokens, 'COLLECTIVE')
+		no_user_modification = check_token(tokens, 'NO-USER-MODIFICATION')
+		usage = AttributeTypeUsage.userApplications
+		if check_token(tokens, 'USAGE'):
+			token = tokens.pop(0)
+			if token == 'userApplications':
+				usage = AttributeTypeUsage.userApplications
+			elif token == 'directoryOperation':
+				usage = AttributeTypeUsage.directoryOperation
+			elif token == 'distributedOperation':
+				usage = AttributeTypeUsage.distributedOperation
+			elif token == 'dSAOperation':
+				usage = AttributeTypeUsage.dSAOperation
+			else:
+				raise ValueError(f'Invalid usage value "{token}"')
+		extensions = parse_extensions(tokens)
+		parse_token(tokens, ')')
+		if tokens:
+			raise ValueError(f'Unexpected token "{tokens[0]}", expected no more input')
+		return cls(oid, name=name, desc=desc, obsolete=obsolete, sup=sup,
+		           equality=equality, ordering=ordering, substr=substr,
+		           syntax=syntax, syntax_len=syntax_len, single_value=single_value,
+		           collective=collective, no_user_modification=no_user_modification,
+		           usage=usage, extensions=extensions)
+
+	@property
+	def first_component_oid(self):
+		return self.oid
+
+class ObjectClassKind(enum.Enum):
+	'''Values for kind argument of `ObjectClass`'''
+	ABSTRACT = enum.auto()
+	STRUCTURAL = enum.auto()
+	AUXILIARY = enum.auto()
+
+class ObjectClassDefinition:
+	# pylint: disable=too-many-arguments
+	def __init__(self, oid, name=None, desc='', obsolete=False, sup=None,
+	             kind=ObjectClassKind.STRUCTURAL, must=None, may=None,
+	             extensions=None):
+		self.oid = oid
+		self.name = name or []
+		self.desc = desc
+		self.obsolete = obsolete
+		self.sup = sup or []
+		self.kind = kind
+		self.must = must or []
+		self.may = may or []
+		self.extensions = extensions or {}
+
+	def __str__(self):
+		tokens = ['(', self.oid]
+		if self.name:
+			tokens += ['NAME'] + qdescrs_to_tokens(self.name)
+		if self.desc:
+			tokens += ['DESC', qdstring_to_token(self.desc)]
+		if self.obsolete:
+			tokens += ['OBSOLETE']
+		if self.sup:
+			tokens += ['SUP'] + oids_to_tokens(self.sup)
+		tokens += [self.kind.name]
+		if self.must:
+			tokens += ['MUST'] + oids_to_tokens(self.must)
+		if self.may:
+			tokens += ['MAY'] + oids_to_tokens(self.may)
+		tokens += extensions_to_tokens(self.extensions) + [')']
+		return ' '.join(tokens)
+
+	@classmethod
+	def from_str(cls, string):
+		tokens = tokenize(string)
+		parse_token(tokens, '(')
+		oid = parse_numericoid(tokens)
+		name = []
+		if check_token(tokens, 'NAME'):
+			name = parse_qdescrs(tokens)
+		desc = ''
+		if check_token(tokens, 'DESC'):
+			desc = parse_qdstring(tokens)
+		obsolete = check_token(tokens, 'OBSOLETE')
+		sup = []
+		if check_token(tokens, 'SUP'):
+			sup = parse_oids(tokens)
+		kind = ObjectClassKind.STRUCTURAL
+		if check_token(tokens, 'ABSTRACT'):
+			kind = ObjectClassKind.ABSTRACT
+		elif check_token(tokens, 'STRUCTURAL'):
+			kind = ObjectClassKind.STRUCTURAL
+		elif check_token(tokens, 'AUXILIARY'):
+			kind = ObjectClassKind.AUXILIARY
+		must = []
+		if check_token(tokens, 'MUST'):
+			must = parse_oids(tokens)
+		may = []
+		if check_token(tokens, 'MAY'):
+			may = parse_oids(tokens)
+		extensions = parse_extensions(tokens)
+		parse_token(tokens, ')')
+		if tokens:
+			raise ValueError(f'Unexpected token "{tokens[0]}", expected no more input')
+		return cls(oid, name=name, desc=desc, obsolete=obsolete, sup=sup,
+		           kind=kind, must=must, may=may, extensions=extensions)
+
+	@property
+	def first_component_oid(self):
+		return self.oid
diff --git a/ldapserver/schema/matching_rules.py b/ldapserver/schema/matching_rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..851eb6f759e7d15ad82777711b0e85be7ee6eb6b
--- /dev/null
+++ b/ldapserver/schema/matching_rules.py
@@ -0,0 +1,267 @@
+import re
+
+from .definitions import MatchingRuleDefinition, MatchingRuleKind
+from .. import rfc4518_stringprep, exceptions
+from . import syntaxes
+
+class GenericMatchingRuleDefinition(MatchingRuleDefinition):
+	def match_equal(self, schema, attribute_values, assertion_value):
+		for attribute_value in attribute_values:
+			if attribute_value == assertion_value:
+				return True
+		return False
+
+	def match_less(self, schema, attribute_values, assertion_value):
+		for attribute_value in attribute_values:
+			if attribute_value < assertion_value:
+				return True
+		return False
+
+	def match_greater_or_equal(self, schema, attribute_values, assertion_value):
+		for attribute_value in attribute_values:
+			if attribute_value >= assertion_value:
+				return True
+		return False
+
+def _substr_match(attribute_value, inital_substring, any_substrings, final_substring):
+	if inital_substring:
+		if not attribute_value.startswith(inital_substring):
+			return False
+		attribute_value = attribute_value[len(inital_substring):]
+	if final_substring:
+		if not attribute_value.endswith(final_substring):
+			return False
+		attribute_value = attribute_value[:-len(final_substring)]
+	for substring in any_substrings:
+		index = attribute_value.find(substring)
+		if index == -1:
+			return False
+		attribute_value = attribute_value[index+len(substring):]
+	return True
+
+class StringMatchingRuleDefinition(MatchingRuleDefinition):
+	def __init__(self, oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, **kwargs):
+		super().__init__(oid, **kwargs)
+		self.matching_type = matching_type
+
+	def prepare_assertion_value(self, attribute_value):
+		try:
+			return rfc4518_stringprep.prepare(attribute_value, self.matching_type)
+		except ValueError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax('Assertion value contains characters prohibited by RFC4518') from exc
+
+	def prepare_attribute_value(self, attribute_value):
+		try:
+			return rfc4518_stringprep.prepare(attribute_value, self.matching_type)
+		except ValueError:
+			return None
+
+	def match_equal(self, schema, attribute_values, assertion_value):
+		assertion_value = self.prepare_assertion_value(assertion_value)
+		for attribute_value in attribute_values:
+			attribute_value = self.prepare_attribute_value(attribute_value)
+			if attribute_value == assertion_value:
+				return True
+		return False
+
+	def match_less(self, schema, attribute_values, assertion_value):
+		assertion_value = self.prepare_assertion_value(assertion_value)
+		for attribute_value in attribute_values:
+			attribute_value = self.prepare_attribute_value(attribute_value)
+			if attribute_value < assertion_value:
+				return True
+		return False
+
+	def match_greater_or_equal(self, schema, attribute_values, assertion_value):
+		assertion_value = self.prepare_assertion_value(assertion_value)
+		for attribute_value in attribute_values:
+			attribute_value = self.prepare_attribute_value(attribute_value)
+			if attribute_value >= assertion_value:
+				return True
+		return False
+
+	def match_substr(self, schema, attribute_values, inital_substring, any_substrings, final_substring):
+		try:
+			if inital_substring:
+				inital_substring = rfc4518_stringprep.prepare(inital_substring, self.matching_type, rfc4518_stringprep.SubstringType.INITIAL)
+			any_substrings = [rfc4518_stringprep.prepare(substring, self.matching_type, rfc4518_stringprep.SubstringType.ANY) for substring in any_substrings]
+			if final_substring:
+				final_substring = rfc4518_stringprep.prepare(final_substring, self.matching_type, rfc4518_stringprep.SubstringType.FINAL)
+		except ValueError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax('Assertion value contains characters prohibited by RFC4518') from exc
+		for attribute_value in attribute_values:
+			try:
+				attribute_value = self.prepare_attribute_value(attribute_value)
+				if _substr_match(attribute_value, inital_substring, any_substrings, final_substring):
+					return True
+			except ValueError:
+				pass
+		return False
+
+class StringListMatchingRuleDefinition(MatchingRuleDefinition):
+	def __init__(self, oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, **kwargs):
+		super().__init__(oid, **kwargs)
+		self.matching_type = matching_type
+
+	# Values are both lists of str
+	def match_equal(self, schema, attribute_values, assertion_value):
+		try:
+			assertion_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in assertion_value]
+		except ValueError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax('Assertion value contains characters prohibited by RFC4518') from exc
+		for attribute_value in attribute_values:
+			try:
+				attribute_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in attribute_value]
+				if attribute_value == assertion_value:
+					return True
+			except ValueError:
+				pass
+		return False
+
+	def match_substr(self, schema, attribute_values, inital_substring, any_substrings, final_substring):
+		try:
+			if inital_substring:
+				inital_substring = rfc4518_stringprep.prepare(inital_substring, self.matching_type, rfc4518_stringprep.SubstringType.INITIAL)
+			any_substrings = [rfc4518_stringprep.prepare(substring, self.matching_type, rfc4518_stringprep.SubstringType.ANY) for substring in any_substrings]
+			if final_substring:
+				final_substring = rfc4518_stringprep.prepare(final_substring, self.matching_type, rfc4518_stringprep.SubstringType.FINAL)
+		except ValueError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax('Assertion value contains characters prohibited by RFC4518') from exc
+		for attribute_value in attribute_values:
+			try:
+				# LF is mapped to SPACE by stringprep, so it is suitable as a seperator
+				attribute_value = '\n'.join([rfc4518_stringprep.prepare(line, self.matching_type) for line in attribute_value])
+				if _substr_match(attribute_value, inital_substring, any_substrings, final_substring):
+					return True
+			except ValueError:
+				pass
+		return False
+
+class FirstComponentMatchingRuleDefinition(MatchingRuleDefinition):
+	def __init__(self, oid, attribute_name, matching_rule, compatability_tag=None, **kwargs):
+		compatability_tag = compatability_tag or 'FirstComponent:'+matching_rule.compatability_tag
+		super().__init__(oid, compatability_tag=compatability_tag, **kwargs)
+		self.attribute_name = attribute_name
+		self.matching_rule = matching_rule
+
+	def match_equal(self, schema, attribute_values, assertion_value):
+		attribute_values = [getattr(value, self.attribute_name)
+		                    for value in attribute_values
+		                    if hasattr(value, self.attribute_name)]
+		return self.matching_rule.match_equal(schema, attribute_values, assertion_value)
+
+
+class OIDMatchingRuleDefinition(MatchingRuleDefinition):
+	NUMERIC_OID_RE = re.compile(r"([0-9]|[1-9][0-9]*)(\.([0-9]|[1-9][0-9]*))*")
+
+	def match_equal(self, schema, attribute_values, assertion_value):
+		if not self.NUMERIC_OID_RE.fullmatch(assertion_value):
+			assertion_value = schema.get_numeric_oid(assertion_value)
+		if assertion_value is None:
+			raise exceptions.LDAPInvalidAttributeSyntax('Assertion value is an unknown OID descriptor')
+		for attribute_value in attribute_values:
+			attribute_value = schema.get_numeric_oid(attribute_value, attribute_value)
+			if attribute_value == assertion_value:
+				return True
+		return False
+
+class StubMatchingRuleDefinition(MatchingRuleDefinition):
+	pass
+
+# RFC4517
+bitStringMatch = GenericMatchingRuleDefinition('2.5.13.16', name=['bitStringMatch'], syntax=syntaxes.BitString.oid, kind=MatchingRuleKind.EQUALITY)
+booleanMatch = GenericMatchingRuleDefinition('2.5.13.13', name=['booleanMatch'], syntax=syntaxes.Boolean.oid, kind=MatchingRuleKind.EQUALITY)
+caseExactIA5Match = StringMatchingRuleDefinition('1.3.6.1.4.1.1466.109.114.1', name=['caseExactIA5Match'], syntax=syntaxes.IA5String.oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, kind=MatchingRuleKind.EQUALITY)
+caseExactMatch = StringMatchingRuleDefinition('2.5.13.5', name=['caseExactMatch'], syntax=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, kind=MatchingRuleKind.EQUALITY)
+caseExactOrderingMatch = StringMatchingRuleDefinition('2.5.13.6', name=['caseExactOrderingMatch'], syntax=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, kind=MatchingRuleKind.ORDERING)
+caseExactSubstringsMatch = StringMatchingRuleDefinition('2.5.13.7', name=['caseExactSubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING, kind=MatchingRuleKind.SUBSTR)
+caseIgnoreIA5Match = StringMatchingRuleDefinition('1.3.6.1.4.1.1466.109.114.2', name=['caseIgnoreIA5Match'], syntax=syntaxes.IA5String.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.EQUALITY)
+caseIgnoreIA5SubstringsMatch = StringMatchingRuleDefinition('1.3.6.1.4.1.1466.109.114.3', name=['caseIgnoreIA5SubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.IA5String.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.SUBSTR)
+caseIgnoreListMatch = StringListMatchingRuleDefinition('2.5.13.11', name=['caseIgnoreListMatch'], syntax=syntaxes.PostalAddress.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.EQUALITY)
+caseIgnoreListSubstringsMatch = StringListMatchingRuleDefinition('2.5.13.12', name=['caseIgnoreListSubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.PostalAddress.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.SUBSTR)
+caseIgnoreMatch = StringMatchingRuleDefinition('2.5.13.2', name=['caseIgnoreMatch'], syntax=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.EQUALITY)
+caseIgnoreOrderingMatch = StringMatchingRuleDefinition('2.5.13.3', name=['caseIgnoreOrderingMatch'], syntax=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.ORDERING)
+caseIgnoreSubstringsMatch = StringMatchingRuleDefinition('2.5.13.4', name=['caseIgnoreSubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.DirectoryString.oid, matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING, kind=MatchingRuleKind.SUBSTR)
+directoryStringFirstComponentMatch = FirstComponentMatchingRuleDefinition('2.5.13.31', name=['directoryStringFirstComponentMatch'], syntax=syntaxes.DirectoryString.oid, attribute_name='first_component_string', matching_rule=caseIgnoreMatch, kind=MatchingRuleKind.EQUALITY)
+distinguishedNameMatch = GenericMatchingRuleDefinition('2.5.13.1', name=['distinguishedNameMatch'], syntax=syntaxes.DN.oid, kind=MatchingRuleKind.EQUALITY)
+generalizedTimeMatch = GenericMatchingRuleDefinition('2.5.13.27', name=['generalizedTimeMatch'], syntax=syntaxes.GeneralizedTime.oid, kind=MatchingRuleKind.EQUALITY)
+generalizedTimeOrderingMatch = GenericMatchingRuleDefinition('2.5.13.28', name=['generalizedTimeOrderingMatch'], syntax=syntaxes.GeneralizedTime.oid, kind=MatchingRuleKind.ORDERING)
+integerMatch = GenericMatchingRuleDefinition('2.5.13.14', name=['integerMatch'], syntax=syntaxes.INTEGER.oid, kind=MatchingRuleKind.EQUALITY)
+integerFirstComponentMatch = FirstComponentMatchingRuleDefinition('2.5.13.29', name=['integerFirstComponentMatch'], syntax=syntaxes.INTEGER.oid, attribute_name='first_component_integer', matching_rule=integerMatch, kind=MatchingRuleKind.EQUALITY)
+integerOrderingMatch = GenericMatchingRuleDefinition('2.5.13.15', name=['integerOrderingMatch'], syntax=syntaxes.INTEGER.oid, kind=MatchingRuleKind.ORDERING)
+# Optional and implementation-specific, we simply never match
+keywordMatch = StubMatchingRuleDefinition('2.5.13.33', name=['keywordMatch'], syntax=syntaxes.DirectoryString.oid, kind=MatchingRuleKind.EQUALITY)
+numericStringMatch = StringMatchingRuleDefinition('2.5.13.8', name=['numericStringMatch'], syntax=syntaxes.NumericString.oid, matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING, kind=MatchingRuleKind.EQUALITY)
+numericStringOrderingMatch = StringMatchingRuleDefinition('2.5.13.9', name=['numericStringOrderingMatch'], syntax=syntaxes.NumericString.oid, matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING, kind=MatchingRuleKind.ORDERING)
+numericStringSubstringsMatch = StringMatchingRuleDefinition('2.5.13.10', name=['numericStringSubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.NumericString.oid, matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING, kind=MatchingRuleKind.SUBSTR)
+objectIdentifierMatch = OIDMatchingRuleDefinition('2.5.13.0', name=['objectIdentifierMatch'], syntax=syntaxes.OID.oid, kind=MatchingRuleKind.EQUALITY)
+objectIdentifierFirstComponentMatch = FirstComponentMatchingRuleDefinition('2.5.13.30', name=['objectIdentifierFirstComponentMatch'], syntax=syntaxes.OID.oid, attribute_name='first_component_oid', matching_rule=objectIdentifierMatch, kind=MatchingRuleKind.EQUALITY)
+octetStringMatch = GenericMatchingRuleDefinition('2.5.13.17', name=['octetStringMatch'], syntax=syntaxes.OctetString.oid, kind=MatchingRuleKind.EQUALITY)
+octetStringOrderingMatch = GenericMatchingRuleDefinition('2.5.13.18', name=['octetStringOrderingMatch'], syntax=syntaxes.OctetString.oid, kind=MatchingRuleKind.ORDERING)
+telephoneNumberMatch = StringMatchingRuleDefinition('2.5.13.20', name=['telephoneNumberMatch'], syntax=syntaxes.TelephoneNumber.oid, matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER, kind=MatchingRuleKind.EQUALITY)
+telephoneNumberSubstringsMatch = StringMatchingRuleDefinition('2.5.13.21', name=['telephoneNumberSubstringsMatch'], syntax=syntaxes.SubstringAssertion.oid, compatability_tag=syntaxes.TelephoneNumber.oid, matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER, kind=MatchingRuleKind.SUBSTR)
+uniqueMemberMatch = GenericMatchingRuleDefinition('2.5.13.23', name=['uniqueMemberMatch'], syntax=syntaxes.NameAndOptionalUID.oid, kind=MatchingRuleKind.EQUALITY)
+# Optional and implementation-specific, we simply never match
+wordMatch = StubMatchingRuleDefinition('2.5.13.32', name=['wordMatch'], syntax=syntaxes.DirectoryString.oid, kind=MatchingRuleKind.EQUALITY)
+
+# RFC4523
+certificateExactMatch = StubMatchingRuleDefinition('2.5.13.34', name=['certificateExactMatch'], desc='X.509 Certificate Exact Match', syntax='1.3.6.1.1.15.1', kind=MatchingRuleKind.EQUALITY)
+certificateMatch = StubMatchingRuleDefinition('2.5.13.35', name=['certificateMatch'], desc='X.509 Certificate Match', syntax='1.3.6.1.1.15.2', kind=MatchingRuleKind.EQUALITY)
+certificatePairExactMatch = StubMatchingRuleDefinition('2.5.13.36', name=['certificatePairExactMatch'], desc='X.509 Certificate Pair Exact Match', syntax='1.3.6.1.1.15.3', kind=MatchingRuleKind.EQUALITY)
+certificatePairMatch = StubMatchingRuleDefinition('2.5.13.37', name=['certificatePairMatch'], desc='X.509 Certificate Pair Match', syntax='1.3.6.1.1.15.4', kind=MatchingRuleKind.EQUALITY)
+certificateListExactMatch = StubMatchingRuleDefinition('2.5.13.38', name=['certificateListExactMatch'], desc='X.509 Certificate List Exact Match', syntax='1.3.6.1.1.15.5', kind=MatchingRuleKind.EQUALITY)
+certificateListMatch = StubMatchingRuleDefinition('2.5.13.39', name=['certificateListMatch'], desc='X.509 Certificate List Match', syntax='1.3.6.1.1.15.6', kind=MatchingRuleKind.EQUALITY)
+algorithmIdentifierMatch = StubMatchingRuleDefinition('2.5.13.40', name=['algorithmIdentifierMatch'], desc='X.509 Algorithm Identifier Match', syntax='1.3.6.1.1.15.7', kind=MatchingRuleKind.EQUALITY)
+
+# RFC3112
+authPasswordExactMatch = StubMatchingRuleDefinition('1.3.6.1.4.1.4203.1.2.2', name=['authPasswordExactMatch'], desc='authentication password exact matching rule', syntax='1.3.6.1.4.1.1466.115.121.1.40', kind=MatchingRuleKind.EQUALITY)
+authPasswordMatch = StubMatchingRuleDefinition('1.3.6.1.4.1.4203.1.2.3', name=['authPasswordMatch'], desc='authentication password matching rule', syntax='1.3.6.1.4.1.1466.115.121.1.40', kind=MatchingRuleKind.EQUALITY)
+
+ALL = (
+	# RFC4517
+	bitStringMatch,
+	booleanMatch,
+	caseExactIA5Match,
+	caseExactMatch,
+	caseExactOrderingMatch,
+	caseExactSubstringsMatch,
+	caseIgnoreIA5Match,
+	caseIgnoreIA5SubstringsMatch,
+	caseIgnoreListMatch,
+	caseIgnoreListSubstringsMatch,
+	caseIgnoreMatch,
+	caseIgnoreOrderingMatch,
+	caseIgnoreSubstringsMatch,
+	directoryStringFirstComponentMatch,
+	distinguishedNameMatch,
+	generalizedTimeMatch,
+	generalizedTimeOrderingMatch,
+	integerFirstComponentMatch,
+	integerMatch,
+	integerOrderingMatch,
+	keywordMatch,
+	numericStringMatch,
+	numericStringOrderingMatch,
+	numericStringSubstringsMatch,
+	objectIdentifierFirstComponentMatch,
+	objectIdentifierMatch,
+	octetStringMatch,
+	octetStringOrderingMatch,
+	telephoneNumberMatch,
+	telephoneNumberSubstringsMatch,
+	uniqueMemberMatch,
+	wordMatch,
+
+	# RFC4523
+	certificateExactMatch,
+	certificateMatch,
+	certificatePairExactMatch,
+	certificatePairMatch,
+	certificateListExactMatch,
+	certificateListMatch,
+	algorithmIdentifierMatch,
+
+	# RFC3112
+	authPasswordExactMatch,
+	authPasswordMatch,
+)
diff --git a/ldapserver/schema/rfc1274/__init__.py b/ldapserver/schema/rfc1274/__init__.py
deleted file mode 100644
index 15af5069f5718d05686bc98efb560b97a4006a02..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc1274/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, attribute_types
diff --git a/ldapserver/schema/rfc1274/attribute_types.py b/ldapserver/schema/rfc1274/attribute_types.py
deleted file mode 100644
index c2d5c1246df0c6a6945d47c3e2c6acea62732e03..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc1274/attribute_types.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from ..types import AttributeType
-from . import syntaxes
-
-audio = AttributeType('0.9.2342.19200300.100.1.55', name='audio', desc='audio (u-law)', syntax=syntaxes.OctetString(25000))
-photo = AttributeType('0.9.2342.19200300.100.1.7', name='photo', desc='photo (G3 fax)', syntax=syntaxes.OctetString(25000))
-
-ALL = (
-	audio,
-	photo,
-)
diff --git a/ldapserver/schema/rfc1274/syntaxes.py b/ldapserver/schema/rfc1274/syntaxes.py
deleted file mode 100644
index c03df67ab8c2ad9691c5ca9ab4b12d067bccbd6a..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc1274/syntaxes.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from ..rfc4517.syntaxes import OctetString
-
-ALL = (
-	OctetString,
-)
diff --git a/ldapserver/schema/rfc2079/__init__.py b/ldapserver/schema/rfc2079/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2079/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc2079/attribute_types.py b/ldapserver/schema/rfc2079/attribute_types.py
deleted file mode 100644
index 4991c5a3a056ba5a3f8753e924a03a3d725088c4..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2079/attribute_types.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ..types import AttributeType
-from . import syntaxes, matching_rules
-
-labeledURI = AttributeType('1.3.6.1.4.1.250.1.57', name='labeledURI', desc='Uniform Resource Identifier with optional label', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString())
-
-ALL = (
-	labeledURI,
-)
diff --git a/ldapserver/schema/rfc2079/matching_rules.py b/ldapserver/schema/rfc2079/matching_rules.py
deleted file mode 100644
index 77afc5bb9716470bcd0a2481b1d5fa116e14b2e2..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2079/matching_rules.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from ..rfc4517.matching_rules import caseExactMatch
-
-ALL = (
-	caseExactMatch,
-)
diff --git a/ldapserver/schema/rfc2079/object_classes.py b/ldapserver/schema/rfc2079/object_classes.py
deleted file mode 100644
index 0254395b0aadd0284031fad67c0210008ed751ae..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2079/object_classes.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from ..types import ObjectClass, ObjectClassKind
-from ..rfc4512.object_classes import top
-from . import attribute_types
-
-labeledURIObject = ObjectClass('1.3.6.1.4.1.250.3.15', name='labeledURIObject', desc='object that contains the URI attribute type', sup=top, kind=ObjectClassKind.AUXILIARY, may=[attribute_types.labeledURI])
-
-ALL = (
-	labeledURIObject,
-)
diff --git a/ldapserver/schema/rfc2079/syntaxes.py b/ldapserver/schema/rfc2079/syntaxes.py
deleted file mode 100644
index 5d68139c5fa7703f7bd52035ad0c85784f25a401..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2079/syntaxes.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from ..rfc4517.syntaxes import DirectoryString
-
-ALL = (
-	DirectoryString,
-)
diff --git a/ldapserver/schema/rfc2252/__init__.py b/ldapserver/schema/rfc2252/__init__.py
deleted file mode 100644
index 2b160bc1ac78b9e011eb5ca457b0f858f30257b1..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2252/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes
diff --git a/ldapserver/schema/rfc2252/syntaxes.py b/ldapserver/schema/rfc2252/syntaxes.py
deleted file mode 100644
index cf2d2492dd021dfe34c5d69453aa0862db34de15..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2252/syntaxes.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from ..types import Syntax
-
-# Only deprecated syntaxes from the old LDAP v3 RFCs
-
-class Binary(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.5'
-	desc = 'Binary'
-
-	@staticmethod
-	def encode(schema, value):
-		return value
-
-	@staticmethod
-	def decode(schema, raw_value):
-		return raw_value
-
-ALL = (
-	Binary,
-)
diff --git a/ldapserver/schema/rfc2307bis/__init__.py b/ldapserver/schema/rfc2307bis/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2307bis/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc2307bis/attribute_types.py b/ldapserver/schema/rfc2307bis/attribute_types.py
deleted file mode 100644
index 9cca89031364155e65aa6f2848534abe124e5663..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2307bis/attribute_types.py
+++ /dev/null
@@ -1,77 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import,unused-import
-
-from ..types import AttributeType
-from ..rfc4524.attribute_types import *
-from ..rfc3112.attribute_types import authPassword, ALL as RFC3112_ALL
-from . import syntaxes, matching_rules
-
-uidNumber = AttributeType('1.3.6.1.1.1.1.0', name='uidNumber', desc='An integer uniquely identifying a user in an administrative domain', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-gidNumber = AttributeType('1.3.6.1.1.1.1.1', name='gidNumber', desc='An integer uniquely identifying a group in an administrative domain', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-gecos = AttributeType('1.3.6.1.1.1.1.2', name='gecos', desc='The GECOS field; the common name', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-homeDirectory = AttributeType('1.3.6.1.1.1.1.3', name='homeDirectory', desc='The absolute path to the home directory', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String(), single_value=True)
-loginShell = AttributeType('1.3.6.1.1.1.1.4', name='loginShell', desc='The path to the login shell', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String(), single_value=True)
-shadowLastChange = AttributeType('1.3.6.1.1.1.1.5', name='shadowLastChange', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowMin = AttributeType('1.3.6.1.1.1.1.6', name='shadowMin', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowMax = AttributeType('1.3.6.1.1.1.1.7', name='shadowMax', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowWarning = AttributeType('1.3.6.1.1.1.1.8', name='shadowWarning', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowInactive = AttributeType('1.3.6.1.1.1.1.9', name='shadowInactive', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowExpire = AttributeType('1.3.6.1.1.1.1.10', name='shadowExpire', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-shadowFlag = AttributeType('1.3.6.1.1.1.1.11', name='shadowFlag', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-memberUid = AttributeType('1.3.6.1.1.1.1.12', name='memberUid', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString())
-memberNisNetgroup = AttributeType('1.3.6.1.1.1.1.13', name='memberNisNetgroup', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString())
-nisNetgroupTriple = AttributeType('1.3.6.1.1.1.1.14', name='nisNetgroupTriple', desc='Netgroup triple', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-ipServicePort = AttributeType('1.3.6.1.1.1.1.15', name='ipServicePort', desc='Service port number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-ipServiceProtocol = AttributeType('1.3.6.1.1.1.1.16', name='ipServiceProtocol', desc='Service protocol name', equality=matching_rules.caseIgnoreMatch, syntax=syntaxes.DirectoryString())
-ipProtocolNumber = AttributeType('1.3.6.1.1.1.1.17', name='ipProtocolNumber', desc='IP protocol number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-oncRpcNumber = AttributeType('1.3.6.1.1.1.1.18', name='oncRpcNumber', desc='ONC RPC number', equality=matching_rules.integerMatch, ordering=matching_rules.integerOrderingMatch, syntax=syntaxes.INTEGER(), single_value=True)
-ipHostNumber = AttributeType('1.3.6.1.1.1.1.19', name='ipHostNumber', desc='IPv4 addresses as a dotted decimal omitting leading zeros or IPv6 addresses as defined in RFC2373', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String())
-ipNetworkNumber = AttributeType('1.3.6.1.1.1.1.20', name='ipNetworkNumber', desc='IP network omitting leading zeros, eg. 192.168', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(), single_value=True)
-ipNetmaskNumber = AttributeType('1.3.6.1.1.1.1.21', name='ipNetmaskNumber', desc='IP netmask omitting leading zeros, eg. 255.255.255.0', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(), single_value=True)
-macAddress = AttributeType('1.3.6.1.1.1.1.22', name='macAddress', desc='MAC address in maximal, colon separated hex notation, eg. 00:00:92:90:ee:e2', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String())
-bootParameter = AttributeType('1.3.6.1.1.1.1.23', name='bootParameter', desc='rpc.bootparamd parameter', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String())
-bootFile = AttributeType('1.3.6.1.1.1.1.24', name='bootFile', desc='Boot image name', equality=matching_rules.caseExactIA5Match, syntax=syntaxes.IA5String())
-nisMapName = AttributeType('1.3.6.1.1.1.1.26', name='nisMapName', desc='Name of a generic NIS map', equality=matching_rules.caseIgnoreMatch, syntax=syntaxes.DirectoryString(64))
-nisMapEntry = AttributeType('1.3.6.1.1.1.1.27', name='nisMapEntry', desc='A generic NIS entry', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(1024), single_value=True)
-nisPublicKey = AttributeType('1.3.6.1.1.1.1.28', name='nisPublicKey', desc='NIS public key', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString(), single_value=True)
-nisSecretKey = AttributeType('1.3.6.1.1.1.1.29', name='nisSecretKey', desc='NIS secret key', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString(), single_value=True)
-nisDomain = AttributeType('1.3.6.1.1.1.1.30', name='nisDomain', desc='NIS domain', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(256))
-automountMapName = AttributeType('1.3.6.1.1.1.1.31', name='automountMapName', desc='automount Map Name', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-automountKey = AttributeType('1.3.6.1.1.1.1.32', name='automountKey', desc='Automount Key value', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-automountInformation = AttributeType('1.3.6.1.1.1.1.33', name='automountInformation', desc='Automount information', equality=matching_rules.caseExactMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-
-ALL = ALL + RFC3112_ALL + (
-	uidNumber,
-	gidNumber,
-	gecos,
-	homeDirectory,
-	loginShell,
-	shadowLastChange,
-	shadowMin,
-	shadowMax,
-	shadowWarning,
-	shadowInactive,
-	shadowExpire,
-	shadowFlag,
-	memberUid,
-	memberNisNetgroup,
-	nisNetgroupTriple,
-	ipServicePort,
-	ipServiceProtocol,
-	ipProtocolNumber,
-	oncRpcNumber,
-	ipHostNumber,
-	ipNetworkNumber,
-	ipNetmaskNumber,
-	macAddress,
-	bootParameter,
-	bootFile,
-	nisMapName,
-	nisMapEntry,
-	nisPublicKey,
-	nisSecretKey,
-	nisDomain,
-	automountMapName,
-	automountKey,
-	automountInformation,
-)
diff --git a/ldapserver/schema/rfc2307bis/matching_rules.py b/ldapserver/schema/rfc2307bis/matching_rules.py
deleted file mode 100644
index adeaf47846621a693be1f49fb208577e489ca70b..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2307bis/matching_rules.py
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4524.matching_rules import *
-from ..rfc3112.matching_rules import ALL as RFC3112_ALL
-
-ALL = ALL + RFC3112_ALL
diff --git a/ldapserver/schema/rfc2307bis/object_classes.py b/ldapserver/schema/rfc2307bis/object_classes.py
deleted file mode 100644
index f61e7d2c93e14a37a52fa7c5a2100812e3ac0301..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2307bis/object_classes.py
+++ /dev/null
@@ -1,47 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import ObjectClass, ObjectClassKind
-from ..rfc3112.object_classes import ALL as RFC3112_ALL
-from ..rfc4524.object_classes import *
-from . import attribute_types
-
-posixAccount = ObjectClass('1.3.6.1.1.1.2.0', name='posixAccount', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of an account with POSIX attributes', must=[attribute_types.cn, attribute_types.uid, attribute_types.uidNumber, attribute_types.gidNumber, attribute_types.homeDirectory], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.loginShell, attribute_types.gecos, attribute_types.description])
-shadowAccount = ObjectClass('1.3.6.1.1.1.2.1', name='shadowAccount', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Additional attributes for shadow passwords', must=[attribute_types.uid], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.description, attribute_types.shadowLastChange, attribute_types.shadowMin, attribute_types.shadowMax, attribute_types.shadowWarning, attribute_types.shadowInactive, attribute_types.shadowExpire, attribute_types.shadowFlag])
-posixGroup = ObjectClass('1.3.6.1.1.1.2.2', name='posixGroup', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of a group of accounts', must=[attribute_types.gidNumber], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.memberUid, attribute_types.description])
-ipService = ObjectClass('1.3.6.1.1.1.2.3', name='ipService', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction an Internet Protocol service.  Maps an IP port and protocol (such as tcp or udp) to one or more names; the distinguished value of the cn attribute denotes the service\'s canonical name', must=[attribute_types.cn, attribute_types.ipServicePort, attribute_types.ipServiceProtocol], may=[attribute_types.description])
-ipProtocol = ObjectClass('1.3.6.1.1.1.2.4', name='ipProtocol', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of an IP protocol. Maps a protocol number to one or more names. The distinguished value of the cn attribute denotes the protocol canonical name', must=[attribute_types.cn, attribute_types.ipProtocolNumber], may=[attribute_types.description])
-oncRpc = ObjectClass('1.3.6.1.1.1.2.5', name='oncRpc', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of an Open Network Computing (ONC) [RFC1057] Remote Procedure Call (RPC) binding.  This class maps an ONC RPC number to a name.  The distinguished value of the cn attribute denotes the RPC service canonical name', must=[attribute_types.cn, attribute_types.oncRpcNumber], may=[attribute_types.description])
-ipHost = ObjectClass('1.3.6.1.1.1.2.6', name='ipHost', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Abstraction of a host, an IP device. The distinguished value of the cn attribute denotes the host\'s canonical name. Device SHOULD be used as a structural class', must=[attribute_types.cn, attribute_types.ipHostNumber], may=[attribute_types.authPassword, attribute_types.userPassword, attribute_types.l, attribute_types.description, attribute_types.manager])
-ipNetwork = ObjectClass('1.3.6.1.1.1.2.7', name='ipNetwork', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of a network. The distinguished value of the cn attribute denotes the network canonical name', must=[attribute_types.ipNetworkNumber], may=[attribute_types.cn, attribute_types.ipNetmaskNumber, attribute_types.l, attribute_types.description, attribute_types.manager])
-nisNetgroup = ObjectClass('1.3.6.1.1.1.2.8', name='nisNetgroup', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Abstraction of a netgroup. May refer to other netgroups', must=[attribute_types.cn], may=[attribute_types.nisNetgroupTriple, attribute_types.memberNisNetgroup, attribute_types.description])
-nisMap = ObjectClass('1.3.6.1.1.1.2.9', name='nisMap', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='A generic abstraction of a NIS map', must=[attribute_types.nisMapName], may=[attribute_types.description])
-nisObject = ObjectClass('1.3.6.1.1.1.2.10', name='nisObject', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='An entry in a NIS map', must=[attribute_types.cn, attribute_types.nisMapEntry, attribute_types.nisMapName])
-ieee802Device = ObjectClass('1.3.6.1.1.1.2.11', name='ieee802Device', sup=top, kind=ObjectClassKind.AUXILIARY, desc='A device with a MAC address; device SHOULD be used as a structural class', may=[attribute_types.macAddress])
-bootableDevice = ObjectClass('1.3.6.1.1.1.2.12', name='bootableDevice', sup=top, kind=ObjectClassKind.AUXILIARY, desc='A device with boot parameters; device SHOULD be used as a structural class', may=[attribute_types.bootFile, attribute_types.bootParameter])
-nisKeyObject = ObjectClass('1.3.6.1.1.1.2.14', name='nisKeyObject', sup=top, kind=ObjectClassKind.AUXILIARY, desc='An object with a public and secret key', must=[attribute_types.cn, attribute_types.nisPublicKey, attribute_types.nisSecretKey], may=[attribute_types.uidNumber, attribute_types.description])
-nisDomainObject = ObjectClass('1.3.6.1.1.1.2.15', name='nisDomainObject', sup=top, kind=ObjectClassKind.AUXILIARY, desc='Associates a NIS domain with a naming context', must=[attribute_types.nisDomain])
-automountMap = ObjectClass('1.3.6.1.1.1.2.16', name='automountMap', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.automountMapName], may=[attribute_types.description])
-automount = ObjectClass('1.3.6.1.1.1.2.17', name='automount', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='Automount information', must=[attribute_types.automountKey, attribute_types.automountInformation], may=[attribute_types.description])
-groupOfMembers = ObjectClass('1.3.6.1.1.1.2.18', name='groupOfMembers', sup=top, kind=ObjectClassKind.STRUCTURAL, desc='A group with members (DNs)', must=[attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description, attribute_types.member])
-
-ALL = ALL + RFC3112_ALL + (
-	posixAccount,
-	shadowAccount,
-	posixGroup,
-	ipService,
-	ipProtocol,
-	oncRpc,
-	ipHost,
-	ipNetwork,
-	nisNetgroup,
-	nisMap,
-	nisObject,
-	ieee802Device,
-	bootableDevice,
-	nisKeyObject,
-	nisDomainObject,
-	automountMap,
-	automount,
-	groupOfMembers,
-)
diff --git a/ldapserver/schema/rfc2307bis/syntaxes.py b/ldapserver/schema/rfc2307bis/syntaxes.py
deleted file mode 100644
index 8156f9b078969b2b4651bd7cdb2f730dd22be95f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2307bis/syntaxes.py
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4524.syntaxes import *
-from ..rfc3112.syntaxes import ALL as RFC3112_ALL
-
-ALL = ALL + RFC3112_ALL
diff --git a/ldapserver/schema/rfc2798/__init__.py b/ldapserver/schema/rfc2798/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2798/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc2798/attribute_types.py b/ldapserver/schema/rfc2798/attribute_types.py
deleted file mode 100644
index 67ecd33392fe9468cc8724935ab5c6310ce4ca49..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2798/attribute_types.py
+++ /dev/null
@@ -1,37 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import,unused-import
-
-from ..types import AttributeType
-# RFC2798 is originally based on the old LDAPv3 RFC2256, the old
-# COSINE RFC1274 and RFC2079 (for labeledURI). RFC2256 and RFC1274
-# were obsoleted by RFC4524 and RFC4519. They also updated RFC2798.
-from ..rfc4524.attribute_types import *
-from ..rfc2079.attribute_types import labeledURI, ALL as RFC2079_ALL
-from ..rfc4523.attribute_types import userCertificate
-from ..rfc1274.attribute_types import audio, photo
-from . import syntaxes, matching_rules
-
-carLicense = AttributeType('2.16.840.1.113730.3.1.1', name='carLicense', desc='vehicle license or registration plate', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-departmentNumber = AttributeType('2.16.840.1.113730.3.1.2', name='departmentNumber', desc='identifies a department within an organization', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-displayName = AttributeType('2.16.840.1.113730.3.1.241', name='displayName', desc='preferred name of a person to be used when displaying entries', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-employeeNumber = AttributeType('2.16.840.1.113730.3.1.3', name='employeeNumber', desc='numerically identifies an employee within an organization', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-employeeType = AttributeType('2.16.840.1.113730.3.1.4', name='employeeType', desc='type of employment for a person', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-jpegPhoto = AttributeType('0.9.2342.19200300.100.1.60', name='jpegPhoto', desc='a JPEG image', syntax=syntaxes.JPEG())
-preferredLanguage = AttributeType('2.16.840.1.113730.3.1.39', name='preferredLanguage', desc='preferred written or spoken language for a person', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(), single_value=True)
-userSMIMECertificate = AttributeType('2.16.840.1.113730.3.1.40', name='userSMIMECertificate', desc='PKCS#7 SignedData used to support S/MIME', syntax=syntaxes.Binary())
-userPKCS12 = AttributeType('2.16.840.1.113730.3.1.216', name='userPKCS12', desc='PKCS #12 PFX PDU for exchange of personal identity information', syntax=syntaxes.Binary())
-
-ALL = ALL + RFC2079_ALL + (
-	userCertificate, # RFC4523
-	audio, # RFC1274
-	photo, # RFC1274
-	carLicense,
-	departmentNumber,
-	displayName,
-	employeeNumber,
-	employeeType,
-	jpegPhoto,
-	preferredLanguage,
-	userSMIMECertificate,
-	userPKCS12,
-)
diff --git a/ldapserver/schema/rfc2798/matching_rules.py b/ldapserver/schema/rfc2798/matching_rules.py
deleted file mode 100644
index cea092d5209d391c17d48ec1cf36a8e32aa8bf09..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2798/matching_rules.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4524.matching_rules import *
diff --git a/ldapserver/schema/rfc2798/object_classes.py b/ldapserver/schema/rfc2798/object_classes.py
deleted file mode 100644
index 370759b973e9720f3138f7ce3dee653411d7d879..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2798/object_classes.py
+++ /dev/null
@@ -1,12 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import ObjectClass, ObjectClassKind
-from ..rfc4524.object_classes import *
-from . import attribute_types
-
-inetOrgPerson = ObjectClass('2.16.840.1.113730.3.2.2', name='inetOrgPerson', sup=organizationalPerson, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.businessCategory, attribute_types.carLicense, attribute_types.departmentNumber, attribute_types.displayName, attribute_types.employeeNumber, attribute_types.employeeType, attribute_types.givenName, attribute_types.homePhone, attribute_types.homePostalAddress, attribute_types.initials, attribute_types.jpegPhoto, attribute_types.labeledURI, attribute_types.mail, attribute_types.manager, attribute_types.mobile, attribute_types.o, attribute_types.pager, attribute_types.roomNumber, attribute_types.secretary, attribute_types.uid, attribute_types.x500UniqueIdentifier, attribute_types.preferredLanguage, attribute_types.userSMIMECertificate, attribute_types.userPKCS12, attribute_types.userCertificate, attribute_types.audio, attribute_types.photo])
-
-ALL = ALL + (
-	inetOrgPerson,
-)
diff --git a/ldapserver/schema/rfc2798/syntaxes.py b/ldapserver/schema/rfc2798/syntaxes.py
deleted file mode 100644
index ee6a435aef6727cf010626c2b00d1afa7f1d02f1..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc2798/syntaxes.py
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4524.syntaxes import *
-from ..rfc2252.syntaxes import Binary
-
-ALL = ALL + (Binary,)
diff --git a/ldapserver/schema/rfc3112/__init__.py b/ldapserver/schema/rfc3112/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc3112/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc3112/attribute_types.py b/ldapserver/schema/rfc3112/attribute_types.py
deleted file mode 100644
index 534b574c2ef72089ca04bb4f1929dfa30083b49c..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc3112/attribute_types.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from ..types import AttributeType, AttributeTypeUsage
-from . import syntaxes, matching_rules
-
-supportedAuthPasswordSchemes = AttributeType('1.3.6.1.4.1.4203.1.3.3', name='supportedAuthPasswordSchemes', desc='supported password storage schemes', equality=matching_rules.caseIgnoreIA5Match, syntax=syntaxes.IA5String(32), usage=AttributeTypeUsage.dSAOperation)
-authPassword = AttributeType('1.3.6.1.4.1.4203.1.3.4', name='authPassword', desc='password authentication information', equality=matching_rules.authPasswordExactMatch, syntax=syntaxes.AuthPasswordSyntax())
-
-ALL = (
-	supportedAuthPasswordSchemes,
-	authPassword,
-)
diff --git a/ldapserver/schema/rfc3112/matching_rules.py b/ldapserver/schema/rfc3112/matching_rules.py
deleted file mode 100644
index d0aee5f6dfaf2ccca5134816b898f103537cdaba..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc3112/matching_rules.py
+++ /dev/null
@@ -1,16 +0,0 @@
-
-# pylint: disable=unused-import
-
-from ..types import MatchingRule
-from ..rfc4517.matching_rules import caseIgnoreIA5Match
-from . import syntaxes
-
-authPasswordExactMatch = MatchingRule('1.3.6.1.4.1.4203.1.2.2', name='authPasswordExactMatch', desc='authentication password exact matching rule', syntax=syntaxes.AuthPasswordSyntax())
-
-# We won't implement any actual schemes here, so the default behaviour of MatchingRule (return UNDEFINED) is fine.
-authPasswordMatch = MatchingRule('1.3.6.1.4.1.4203.1.2.3', name='authPasswordMatch', desc='authentication password matching rule', syntax=syntaxes.OctetString(128))
-
-ALL = (
-	authPasswordExactMatch,
-	authPasswordMatch,
-)
diff --git a/ldapserver/schema/rfc3112/object_classes.py b/ldapserver/schema/rfc3112/object_classes.py
deleted file mode 100644
index 749d91ff0c3a101f5d4f3909b6cfa0dd1ba08910..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc3112/object_classes.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ..types import ObjectClass, ObjectClassKind
-from . import attribute_types
-
-authPasswordObject = ObjectClass('1.3.6.1.4.1.4203.1.4.7', name='authPasswordObject', desc='authentication password mix in class', kind=ObjectClassKind.AUXILIARY,  may=[attribute_types.authPassword])
-
-ALL = (
-	authPasswordObject,
-)
diff --git a/ldapserver/schema/rfc3112/syntaxes.py b/ldapserver/schema/rfc3112/syntaxes.py
deleted file mode 100644
index c954aef0b57f8c7ca904a573c734fa6cd8620010..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc3112/syntaxes.py
+++ /dev/null
@@ -1,12 +0,0 @@
-
-# pylint: disable=unused-import
-
-from ..rfc4517.syntaxes import IA5String, OctetString, BytesSyntax
-
-class AuthPasswordSyntax(BytesSyntax):
-	oid = '1.3.6.1.4.1.4203.1.1.2'
-	desc = 'authentication password syntax'
-
-ALL = (
-	AuthPasswordSyntax,
-)
diff --git a/ldapserver/schema/rfc4512/__init__.py b/ldapserver/schema/rfc4512/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4512/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc4512/attribute_types.py b/ldapserver/schema/rfc4512/attribute_types.py
deleted file mode 100644
index dc1f34005b098a9e29f144d195b5f05dca926360..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4512/attribute_types.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from ..types import AttributeType, AttributeTypeUsage
-from . import syntaxes, matching_rules
-
-aliasedObjectName = AttributeType('2.5.4.1', name='aliasedObjectName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True)
-objectClass = AttributeType('2.5.4.0', name='objectClass', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID())
-creatorsName = AttributeType('2.5.18.3', name='creatorsName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-createTimestamp = AttributeType('2.5.18.1', name='createTimestamp', equality=matching_rules.generalizedTimeMatch, ordering=matching_rules.generalizedTimeOrderingMatch, syntax=syntaxes.GeneralizedTime(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-modifiersName = AttributeType('2.5.18.4', name='modifiersName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-modifyTimestamp = AttributeType('2.5.18.2', name='modifyTimestamp', equality=matching_rules.generalizedTimeMatch, ordering=matching_rules.generalizedTimeOrderingMatch, syntax=syntaxes.GeneralizedTime(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-structuralObjectClass = AttributeType('2.5.21.9', name='structuralObjectClass', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-governingStructureRule = AttributeType('2.5.21.10', name='governingStructureRule', equality=matching_rules.integerMatch, syntax=syntaxes.INTEGER(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-subschemaSubentry = AttributeType('2.5.18.10', name='subschemaSubentry', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN(), single_value=True, no_user_modification=True, usage=AttributeTypeUsage.directoryOperation)
-objectClasses = AttributeType('2.5.21.6', name='objectClasses', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.ObjectClassDescription(), usage=AttributeTypeUsage.directoryOperation)
-attributeTypes = AttributeType('2.5.21.5', name='attributeTypes', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.AttributeTypeDescription(), usage=AttributeTypeUsage.directoryOperation)
-matchingRules = AttributeType('2.5.21.4', name='matchingRules', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.MatchingRuleDescription(), usage=AttributeTypeUsage.directoryOperation)
-matchingRuleUse = AttributeType('2.5.21.8', name='matchingRuleUse', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.MatchingRuleUseDescription(), usage=AttributeTypeUsage.directoryOperation)
-ldapSyntaxes = AttributeType('1.3.6.1.4.1.1466.101.120.16', name='ldapSyntaxes', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.LDAPSyntaxDescription(), usage=AttributeTypeUsage.directoryOperation)
-dITContentRules = AttributeType('2.5.21.2', name='dITContentRules', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.DITContentRuleDescription(), usage=AttributeTypeUsage.directoryOperation)
-dITStructureRules = AttributeType('2.5.21.1', name='dITStructureRules', equality=matching_rules.integerFirstComponentMatch, syntax=syntaxes.DITStructureRuleDescription(), usage=AttributeTypeUsage.directoryOperation)
-nameForms = AttributeType('2.5.21.7', name='nameForms', equality=matching_rules.objectIdentifierFirstComponentMatch, syntax=syntaxes.NameFormDescription(), usage=AttributeTypeUsage.directoryOperation)
-altServer = AttributeType('1.3.6.1.4.1.1466.101.120.6', name='altServer', syntax=syntaxes.IA5String(), usage=AttributeTypeUsage.dSAOperation)
-namingContexts = AttributeType('1.3.6.1.4.1.1466.101.120.5', name='namingContexts', syntax=syntaxes.DN(), usage=AttributeTypeUsage.dSAOperation)
-supportedControl = AttributeType('1.3.6.1.4.1.1466.101.120.13', name='supportedControl', syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation)
-supportedExtension = AttributeType('1.3.6.1.4.1.1466.101.120.7', name='supportedExtension', syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation)
-supportedFeatures = AttributeType('1.3.6.1.4.1.4203.1.3.5', name='supportedFeatures', equality=matching_rules.objectIdentifierMatch, syntax=syntaxes.OID(), usage=AttributeTypeUsage.dSAOperation)
-supportedLDAPVersion = AttributeType('1.3.6.1.4.1.1466.101.120.15', name='supportedLDAPVersion', syntax=syntaxes.INTEGER(), usage=AttributeTypeUsage.dSAOperation)
-supportedSASLMechanisms = AttributeType('1.3.6.1.4.1.1466.101.120.14', name='supportedSASLMechanisms', syntax=syntaxes.DirectoryString(), usage=AttributeTypeUsage.dSAOperation)
-
-ALL = (
-	aliasedObjectName,
-	objectClass,
-	creatorsName,
-	createTimestamp,
-	modifiersName,
-	modifyTimestamp,
-	structuralObjectClass,
-	governingStructureRule,
-	subschemaSubentry,
-	objectClasses,
-	attributeTypes,
-	matchingRules,
-	matchingRuleUse,
-	ldapSyntaxes,
-	dITContentRules,
-	dITStructureRules,
-	nameForms,
-	altServer,
-	namingContexts,
-	supportedControl,
-	supportedExtension,
-	supportedFeatures,
-	supportedLDAPVersion,
-	supportedSASLMechanisms,
-)
diff --git a/ldapserver/schema/rfc4512/matching_rules.py b/ldapserver/schema/rfc4512/matching_rules.py
deleted file mode 100644
index ee6bd4a191364fabd69bfe9ea118d6d6fe2eec1d..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4512/matching_rules.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4517.matching_rules import *
diff --git a/ldapserver/schema/rfc4512/object_classes.py b/ldapserver/schema/rfc4512/object_classes.py
deleted file mode 100644
index 8f8f9d35457bcec909db0919d4c9c83481d5eaf9..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4512/object_classes.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from ..types import ObjectClass, ObjectClassKind
-from . import attribute_types
-
-top = ObjectClass('2.5.6.0', 'top', kind=ObjectClassKind.ABSTRACT, must=[attribute_types.objectClass])
-alias = ObjectClass('2.5.6.1', 'alias', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.aliasedObjectName])
-subschema = ObjectClass('2.5.20.1', 'subschema', kind=ObjectClassKind.AUXILIARY, may=[attribute_types.dITStructureRules, attribute_types.nameForms, attribute_types.dITContentRules, attribute_types.objectClasses, attribute_types.attributeTypes, attribute_types.matchingRules, attribute_types.matchingRuleUse])
-extensibleObject = ObjectClass('1.3.6.1.4.1.1466.101.120.111', 'extensibleObject', sup=top, kind=ObjectClassKind.AUXILIARY)
-
-ALL = (
-	top,
-	alias,
-	subschema,
-	extensibleObject,
-)
diff --git a/ldapserver/schema/rfc4512/syntaxes.py b/ldapserver/schema/rfc4512/syntaxes.py
deleted file mode 100644
index 657f66ef37f6ad42639e97196fcefc5fa041e52b..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4512/syntaxes.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4517.syntaxes import *
diff --git a/ldapserver/schema/rfc4517/__init__.py b/ldapserver/schema/rfc4517/__init__.py
deleted file mode 100644
index 051bbed218cb085261e986c436a385eede5eb342..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4517/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules
diff --git a/ldapserver/schema/rfc4517/matching_rules.py b/ldapserver/schema/rfc4517/matching_rules.py
deleted file mode 100644
index ea211434fcf32f889b97ed9c3261ffce6c3314f3..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4517/matching_rules.py
+++ /dev/null
@@ -1,170 +0,0 @@
-from ..types import MatchingRule
-from ... import rfc4518_stringprep
-from . import syntaxes
-
-class GenericMatchingRule(MatchingRule):
-	def match_equal(self, schema, attribute_value, assertion_value):
-		return attribute_value == assertion_value
-
-	def match_less(self, schema, attribute_value, assertion_value):
-		return attribute_value < assertion_value
-
-	def match_greater_or_equal(self, schema, attribute_value, assertion_value):
-		return attribute_value >= assertion_value
-
-class StringMatchingRule(MatchingRule):
-	def __init__(self, oid, name, syntax, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING):
-		super().__init__(oid, name, syntax)
-		self.matching_type = matching_type
-
-	def match_equal(self, schema, attribute_value, assertion_value):
-		try:
-			attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type)
-			assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type)
-		except ValueError:
-			return None
-		return attribute_value == assertion_value
-
-	def match_less(self, schema, attribute_value, assertion_value):
-		try:
-			attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type)
-			assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type)
-		except ValueError:
-			return None
-		return attribute_value < assertion_value
-
-	def match_greater_or_equal(self, schema, attribute_value, assertion_value):
-		try:
-			attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type)
-			assertion_value = rfc4518_stringprep.prepare(assertion_value, self.matching_type)
-		except ValueError:
-			return None
-		return attribute_value >= assertion_value
-
-	def match_substr(self, schema, attribute_value, inital_substring, any_substrings, final_substring):
-		try:
-			attribute_value = rfc4518_stringprep.prepare(attribute_value, self.matching_type)
-			if inital_substring:
-				inital_substring = rfc4518_stringprep.prepare(inital_substring, self.matching_type, rfc4518_stringprep.SubstringType.INITIAL)
-			any_substrings = [rfc4518_stringprep.prepare(substring, self.matching_type, rfc4518_stringprep.SubstringType.ANY) for substring in any_substrings]
-			if final_substring:
-				final_substring = rfc4518_stringprep.prepare(final_substring, self.matching_type, rfc4518_stringprep.SubstringType.FINAL)
-		except ValueError:
-			return None
-		if inital_substring:
-			if not attribute_value.startswith(inital_substring):
-				return False
-			attribute_value = attribute_value[len(inital_substring):]
-		if final_substring:
-			if not attribute_value.endswith(final_substring):
-				return False
-			attribute_value = attribute_value[:-len(final_substring)]
-		for substring in any_substrings:
-			index = attribute_value.find(substring)
-			if index == -1:
-				return False
-			attribute_value = attribute_value[index+len(substring):]
-		return True
-
-class StringListMatchingRule(MatchingRule):
-	def __init__(self, oid, name, syntax, matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING):
-		super().__init__(oid, name, syntax)
-		self.matching_type = matching_type
-
-	# Values are both lists of str
-	def match_equal(self, schema, attribute_value, assertion_value):
-		try:
-			attribute_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in attribute_value]
-			assertion_value = [rfc4518_stringprep.prepare(line, self.matching_type) for line in assertion_value]
-		except ValueError:
-			return None
-		return attribute_value == assertion_value
-
-class FirstComponentMatchingRule(MatchingRule):
-	def __init__(self, oid, name, syntax, attribute_name, matching_rule):
-		super().__init__(oid, name, syntax)
-		self.attribute_name = attribute_name
-		self.matching_rule = matching_rule
-
-	def match_equal(self, schema, attribute_value, assertion_value):
-		if not hasattr(attribute_value, self.attribute_name):
-			return None
-		return self.matching_rule.match_equal(schema, getattr(attribute_value, self.attribute_name)(), assertion_value)
-
-class OIDMatchingRule(MatchingRule):
-	def match_equal(self, schema, attribute_value, assertion_value):
-		attribute_value = schema.get_numeric_oid(attribute_value)
-		assertion_value = schema.get_numeric_oid(assertion_value)
-		if assertion_value is None:
-			return None
-		return attribute_value == assertion_value
-
-bitStringMatch = GenericMatchingRule('2.5.13.16', name='bitStringMatch', syntax=syntaxes.BitString())
-booleanMatch = GenericMatchingRule('2.5.13.13', name='booleanMatch', syntax=syntaxes.Boolean())
-caseExactIA5Match = StringMatchingRule('1.3.6.1.4.1.1466.109.114.1', name='caseExactIA5Match', syntax=syntaxes.IA5String(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING)
-caseExactMatch = StringMatchingRule('2.5.13.5', name='caseExactMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING)
-caseExactOrderingMatch = StringMatchingRule('2.5.13.6', name='caseExactOrderingMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING)
-caseExactSubstringsMatch = StringMatchingRule('2.5.13.7', name='caseExactSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.EXACT_STRING)
-caseIgnoreIA5Match = StringMatchingRule('1.3.6.1.4.1.1466.109.114.2', name='caseIgnoreIA5Match', syntax=syntaxes.IA5String(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreIA5SubstringsMatch = StringMatchingRule('1.3.6.1.4.1.1466.109.114.3', name='caseIgnoreIA5SubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreListMatch = StringListMatchingRule('2.5.13.11', name='caseIgnoreListMatch', syntax=syntaxes.PostalAddress(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreListSubstringsMatch = StringListMatchingRule('2.5.13.12', name='caseIgnoreListSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreMatch = StringMatchingRule('2.5.13.2', name='caseIgnoreMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreOrderingMatch = StringMatchingRule('2.5.13.3', name='caseIgnoreOrderingMatch', syntax=syntaxes.DirectoryString(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-caseIgnoreSubstringsMatch = StringMatchingRule('2.5.13.4', name='caseIgnoreSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.CASE_IGNORE_STRING)
-directoryStringFirstComponentMatch = FirstComponentMatchingRule('2.5.13.31', name='directoryStringFirstComponentMatch', syntax=syntaxes.DirectoryString(), attribute_name='get_first_component_string', matching_rule=caseIgnoreMatch)
-distinguishedNameMatch = GenericMatchingRule('2.5.13.1', name='distinguishedNameMatch', syntax=syntaxes.DN())
-generalizedTimeMatch = GenericMatchingRule('2.5.13.27', name='generalizedTimeMatch', syntax=syntaxes.GeneralizedTime())
-generalizedTimeOrderingMatch = GenericMatchingRule('2.5.13.28', name='generalizedTimeOrderingMatch', syntax=syntaxes.GeneralizedTime())
-integerMatch = GenericMatchingRule('2.5.13.14', name='integerMatch', syntax=syntaxes.INTEGER())
-integerFirstComponentMatch = FirstComponentMatchingRule('2.5.13.29', name='integerFirstComponentMatch', syntax=syntaxes.INTEGER(), attribute_name='get_first_component_integer', matching_rule=integerMatch)
-integerOrderingMatch = GenericMatchingRule('2.5.13.15', name='integerOrderingMatch', syntax=syntaxes.INTEGER())
-# Optional and implementation-specific, we simply never match
-keywordMatch = MatchingRule('2.5.13.33', name='keywordMatch', syntax=syntaxes.DirectoryString())
-numericStringMatch = StringMatchingRule('2.5.13.8', name='numericStringMatch', syntax=syntaxes.NumericString(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING)
-numericStringOrderingMatch = StringMatchingRule('2.5.13.9', name='numericStringOrderingMatch', syntax=syntaxes.NumericString(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING)
-numericStringSubstringsMatch = StringMatchingRule('2.5.13.10', name='numericStringSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.NUMERIC_STRING)
-objectIdentifierMatch = OIDMatchingRule('2.5.13.0', name='objectIdentifierMatch', syntax=syntaxes.OID())
-objectIdentifierFirstComponentMatch = FirstComponentMatchingRule('2.5.13.30', name='objectIdentifierFirstComponentMatch', syntax=syntaxes.OID(), attribute_name='get_first_component_oid', matching_rule=objectIdentifierMatch)
-octetStringMatch = GenericMatchingRule('2.5.13.17', name='octetStringMatch', syntax=syntaxes.OctetString())
-octetStringOrderingMatch = GenericMatchingRule('2.5.13.18', name='octetStringOrderingMatch', syntax=syntaxes.OctetString())
-telephoneNumberMatch = StringMatchingRule('2.5.13.20', name='telephoneNumberMatch', syntax=syntaxes.TelephoneNumber(), matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER)
-telephoneNumberSubstringsMatch = StringMatchingRule('2.5.13.21', name='telephoneNumberSubstringsMatch', syntax=syntaxes.SubstringAssertion(), matching_type=rfc4518_stringprep.MatchingType.TELEPHONE_NUMBER)
-uniqueMemberMatch = GenericMatchingRule('2.5.13.23', name='uniqueMemberMatch', syntax=syntaxes.NameAndOptionalUID())
-# Optional and implementation-specific, we simply never match
-wordMatch = MatchingRule('2.5.13.32', name='wordMatch', syntax=syntaxes.DirectoryString())
-
-ALL = (
-	bitStringMatch,
-	booleanMatch,
-	caseExactIA5Match,
-	caseExactMatch,
-	caseExactOrderingMatch,
-	caseExactSubstringsMatch,
-	caseIgnoreIA5Match,
-	caseIgnoreIA5SubstringsMatch,
-	caseIgnoreListMatch,
-	caseIgnoreListSubstringsMatch,
-	caseIgnoreMatch,
-	caseIgnoreOrderingMatch,
-	caseIgnoreSubstringsMatch,
-	directoryStringFirstComponentMatch,
-	distinguishedNameMatch,
-	generalizedTimeMatch,
-	generalizedTimeOrderingMatch,
-	integerFirstComponentMatch,
-	integerMatch,
-	integerOrderingMatch,
-	#keywordMatch,
-	numericStringMatch,
-	numericStringOrderingMatch,
-	numericStringSubstringsMatch,
-	objectIdentifierFirstComponentMatch,
-	objectIdentifierMatch,
-	octetStringMatch,
-	octetStringOrderingMatch,
-	telephoneNumberMatch,
-	telephoneNumberSubstringsMatch,
-	uniqueMemberMatch,
-	#wordMatch,
-)
diff --git a/ldapserver/schema/rfc4517/syntaxes.py b/ldapserver/schema/rfc4517/syntaxes.py
deleted file mode 100644
index ea02285b86ef8f8ee6d3d617ca253666faaeb54e..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4517/syntaxes.py
+++ /dev/null
@@ -1,409 +0,0 @@
-import re
-import datetime
-
-from ..types import Syntax
-from ... import dn
-
-# Base classes
-class StringSyntax(Syntax):
-	@staticmethod
-	def encode(schema, value):
-		return value.encode('utf8')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		return raw_value.decode('utf8')
-
-class BytesSyntax(Syntax):
-	@staticmethod
-	def encode(schema, value):
-		return value
-
-	@staticmethod
-	def decode(schema, raw_value):
-		return raw_value
-
-class SchemaElementSyntax(Syntax):
-	@staticmethod
-	def encode(schema, value):
-		return value.to_definition().encode('utf8')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		return None
-
-# Syntax definitions
-class AttributeTypeDescription(SchemaElementSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.3'
-	desc = 'Attribute Type Description'
-
-class BitString(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.6'
-	desc = 'Bit String'
-
-class Boolean(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.7'
-	desc = 'Boolean'
-
-	@staticmethod
-	def encode(schema, value):
-		return b'TRUE' if value else b'FALSE'
-
-	@staticmethod
-	def decode(schema, raw_value):
-		if raw_value == b'TRUE':
-			return True
-		elif raw_value == b'FALSE':
-			return False
-		else:
-			return None
-
-class CountryString(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.11'
-	desc = 'Country String'
-
-class DeliveryMethod(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.14'
-	desc = 'Delivery Method'
-
-class DirectoryString(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.15'
-	desc = 'Directory String'
-
-class DITContentRuleDescription(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.16'
-	desc = 'DIT Content Rule Description'
-
-class DITStructureRuleDescription(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.17'
-	desc = 'DIT Structure Rule Description'
-
-class DN(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.12'
-	desc = 'DN'
-
-	@staticmethod
-	def encode(schema, value):
-		return str(value).encode('utf8')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		try:
-			return dn.DN.from_str(raw_value.decode('utf8'))
-		except (UnicodeDecodeError, TypeError, ValueError):
-			return None
-
-class EnhancedGuide(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.21'
-	desc = 'Enhanced Guide'
-
-class FacsimileTelephoneNumber(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.22'
-	desc = 'Facsimile Telephone Number'
-
-class Fax(BytesSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.23'
-	desc = 'Fax'
-
-class GeneralizedTime(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.24'
-	desc = 'Generalized Time'
-
-	@staticmethod
-	def encode(schema, value):
-		str_value = value.strftime('%Y%m%d%H%M%S.%f')
-		if value.tzinfo == datetime.timezone.utc:
-			str_value += 'Z'
-		elif value.tzinfo is not None:
-			delta_seconds = value.tzinfo.utcoffset(value).total_seconds()
-			if delta_seconds < 0:
-				str_value += '-'
-				delta_seconds = -delta_seconds
-			else:
-				str_value += '+'
-			hour = delta_seconds // 3600
-			minute = (delta_seconds % 3600) // 60
-			str_value += '%02d%02d'%(hour, minute)
-		return str_value.encode('ascii')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		try:
-			raw_value = raw_value.decode('utf8')
-		except UnicodeDecodeError:
-			return None
-		match = re.fullmatch(r'([0-9]{10})(|[0-9]{2}|[0-9]{4})(|[,.][0-9]+)(Z|[+-][0-9]{2}|[+-][0-9]{4})', raw_value)
-		if match is None:
-			return None
-		main, minute_second, fraction, timezone = match.groups()
-		fraction = float('0.' + (fraction[1:] or '0'))
-		result = datetime.datetime.strptime(main, '%Y%m%d%H')
-		if not minute_second:
-			result += datetime.timedelta(hours=fraction)
-		if len(minute_second) == 2:
-			result += datetime.timedelta(minutes=int(minute_second)+fraction)
-		elif len(minute_second) == 4:
-			minute = minute_second[:2]
-			second = minute_second[2:4]
-			result += datetime.timedelta(minutes=int(minute), seconds=int(second)+fraction)
-		if timezone == 'Z':
-			result = result.replace(tzinfo=datetime.timezone.utc)
-		elif timezone:
-			sign, hour, minute = timezone[0], timezone[1:3], (timezone[3:5] or '00')
-			delta = datetime.timedelta(hours=int(hour), minutes=int(minute))
-			if sign == '+':
-				result = result.replace(tzinfo=datetime.timezone(delta))
-			else:
-				result = result.replace(tzinfo=datetime.timezone(-delta))
-		return result
-
-class Guide(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.25'
-	desc = 'Guide'
-
-class IA5String(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.26'
-	desc = 'IA5 String'
-
-class INTEGER(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.27'
-	desc = 'INTEGER'
-
-	@staticmethod
-	def encode(schema, value):
-		return str(value).encode('utf8')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		if not raw_value or not raw_value.split(b'-', 1)[-1].isdigit():
-			return None
-		return int(raw_value.decode('utf8'))
-
-class JPEG(BytesSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.28'
-	desc = 'JPEG'
-
-class LDAPSyntaxDescription(SchemaElementSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.54'
-	desc = 'LDAP Syntax Description'
-
-class MatchingRuleDescription(SchemaElementSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.30'
-	desc = 'Matching Rule Description'
-
-class MatchingRuleUseDescription(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.31'
-	desc = 'Matching Rule Use Description'
-
-class NameAndOptionalUID(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.34'
-	desc = 'Name And Optional UID'
-
-	@staticmethod
-	def encode(schema, value):
-		return DN.encode(schema, value)
-
-	@staticmethod
-	def decode(schema, raw_value):
-		escaped = False
-		dn_part = raw_value
-		bitstr_part = b'' # pylint: disable=unused-variable
-		for index, byte in enumerate(raw_value):
-			byte = bytes((byte,))
-			if escaped:
-				escaped = False
-			elif byte == b'\\':
-				escaped = True
-			elif byte == b'#':
-				dn_part = raw_value[:index]
-				bitstr_part = raw_value[index+1:]
-				break
-		# We need to find a good representation of this type, maybe a subclass
-		# of dn.DN that carries the bitstring part as an attribute.
-		#if bitstr_part:
-		#	return DN.decode(dn_part), BitString.decode(bitstr_part)
-		return DN.decode(schema, dn_part)
-
-class NameFormDescription(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.35'
-	desc = 'Name Form Description'
-
-class NumericString(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.36'
-	desc = 'Numeric String'
-
-class ObjectClassDescription(SchemaElementSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.37'
-	desc = 'Object Class Description'
-
-class OctetString(BytesSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.40'
-	desc = 'Octet String'
-
-class OID(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.38'
-	desc = 'OID'
-
-class OtherMailbox(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.39'
-	desc = 'Other Mailbox'
-
-class PostalAddress(Syntax):
-	# 3.3.28.  Postal Address
-	#
-	# A value of the Postal Address syntax is a sequence of strings of one
-	# or more arbitrary UCS characters, which form an address in a physical
-	# mail system.
-	#
-	# The LDAP-specific encoding of a value of this syntax is defined by
-	# the following ABNF:
-	#
-	#
-	#   PostalAddress = line *( DOLLAR line )
-	#   line          = 1*line-char
-	#   line-char     = %x00-23
-	#                   / (%x5C "24")  ; escaped "$"
-	#                   / %x25-5B
-	#                   / (%x5C "5C")  ; escaped "\"
-	#                   / %x5D-7F
-	#                   / UTFMB
-	#
-	# Each character string (i.e., <line>) of a postal address value is
-	# encoded as a UTF-8 [RFC3629] string, except that "\" and "$"
-	# characters, if they occur in the string, are escaped by a "\"
-	# character followed by the two hexadecimal digit code for the
-	# character.  The <DOLLAR> and <UTFMB> rules are defined in [RFC4512].
-	#
-	# Many servers limit the postal address to no more than six lines of no
-	# more than thirty characters each.
-	#
-	#   Example:
-	#      1234 Main St.$Anytown, CA 12345$USA
-	#      \241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA
-	#
-	# The LDAP definition for the Postal Address syntax is:
-	#
-	#   ( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' )
-	#
-	# This syntax corresponds to the PostalAddress ASN.1 type from [X.520];
-	# that is
-	#
-	#   PostalAddress ::= SEQUENCE SIZE(1..ub-postal-line) OF
-	#       DirectoryString { ub-postal-string }
-	#
-	# The values of ub-postal-line and ub-postal-string (both integers) are
-	# implementation defined.  Non-normative definitions appear in [X.520].
-
-	oid = '1.3.6.1.4.1.1466.115.121.1.41'
-	desc = 'Postal Address'
-
-	# Native values are lists of str
-	@staticmethod
-	def encode(schema, value):
-		return '$'.join([line.replace('\\', '\\5C').replace('$', '\\24') for line in value]).encode('utf8')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		return [line.replace('\\24', '$').replace('\\5C', '\\') for line in raw_value.decode('utf8').split('$')]
-
-class PrintableString(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.44'
-	desc = 'Printable String'
-
-class SubstringAssertion(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.58'
-	desc = 'Substring Assertion'
-
-class TelephoneNumber(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.50'
-	desc = 'Telephone Number'
-
-class TeletexTerminalIdentifier(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.51'
-	desc = 'Teletex Terminal Identifier'
-
-class TelexNumber(StringSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.52'
-	desc = 'Telex Number'
-
-class UTCTime(Syntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.53'
-	desc = 'UTC Time'
-
-	@staticmethod
-	def encode(schema, value):
-		str_value = value.strftime('%y%m%d%H%M%S')
-		if value.tzinfo == datetime.timezone.utc:
-			str_value += 'Z'
-		elif value.tzinfo is not None:
-			delta_seconds = value.tzinfo.utcoffset(value).total_seconds()
-			if delta_seconds < 0:
-				str_value += '-'
-				delta_seconds = -delta_seconds
-			else:
-				str_value += '+'
-			hour = delta_seconds // 3600
-			minute = (delta_seconds % 3600) // 60
-			str_value += '%02d%02d'%(hour, minute)
-		return str_value.encode('ascii')
-
-	@staticmethod
-	def decode(schema, raw_value):
-		try:
-			raw_value = raw_value.decode('utf8')
-		except UnicodeDecodeError:
-			return None
-		match = re.fullmatch(r'([0-9]{10})(|[0-9]{2})(|Z|[+-][0-9]{4})', raw_value)
-		if match is None:
-			return None
-		main, seconds, timezone = match.groups()
-		result = datetime.datetime.strptime(main, '%y%m%d%H%M')
-		if seconds:
-			result = result.replace(second=int(seconds))
-		if timezone == 'Z':
-			result = result.replace(tzinfo=datetime.timezone.utc)
-		elif timezone:
-			sign, hour, minute = timezone[0], timezone[1:3], timezone[3:5]
-			delta = datetime.timedelta(hours=int(hour), minutes=int(minute))
-			if sign == '+':
-				result = result.replace(tzinfo=datetime.timezone(delta))
-			else:
-				result = result.replace(tzinfo=datetime.timezone(-delta))
-		return result
-
-ALL = (
-	AttributeTypeDescription,
-	BitString,
-	Boolean,
-	CountryString,
-	DeliveryMethod,
-	DirectoryString,
-	DITContentRuleDescription,
-	DITStructureRuleDescription,
-	DN,
-	EnhancedGuide,
-	FacsimileTelephoneNumber,
-	Fax,
-	GeneralizedTime,
-	Guide,
-	IA5String,
-	INTEGER,
-	JPEG,
-	LDAPSyntaxDescription,
-	MatchingRuleDescription,
-	MatchingRuleUseDescription,
-	NameAndOptionalUID,
-	NameFormDescription,
-	NumericString,
-	ObjectClassDescription,
-	OctetString,
-	OID,
-	OtherMailbox,
-	PostalAddress,
-	PrintableString,
-	SubstringAssertion,
-	TelephoneNumber,
-	TeletexTerminalIdentifier,
-	TelexNumber,
-	UTCTime,
-)
diff --git a/ldapserver/schema/rfc4519/__init__.py b/ldapserver/schema/rfc4519/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4519/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc4519/attribute_types.py b/ldapserver/schema/rfc4519/attribute_types.py
deleted file mode 100644
index d4e474562a577ae1b456d6dfbb45221f8e5212de..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4519/attribute_types.py
+++ /dev/null
@@ -1,96 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import AttributeType
-from ..rfc4512.attribute_types import *
-from . import syntaxes, matching_rules
-
-name = AttributeType('2.5.4.41', name='name', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString()) # Defined first, so sup=name works
-businessCategory = AttributeType('2.5.4.15', name='businessCategory', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-c = AttributeType('2.5.4.6', name='c', sup=name, syntax=syntaxes.CountryString(), single_value=True)
-cn = AttributeType('2.5.4.3', name='cn', sup=name)
-dc = AttributeType('0.9.2342.19200300.100.1.25', name='dc', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String(), single_value=True)
-description = AttributeType('2.5.4.13', name='description', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-destinationIndicator = AttributeType('2.5.4.27', name='destinationIndicator', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString())
-distinguishedName = AttributeType('2.5.4.49', name='distinguishedName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN())
-dnQualifier = AttributeType('2.5.4.46', name='dnQualifier', equality=matching_rules.caseIgnoreMatch, ordering=matching_rules.caseIgnoreOrderingMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString())
-enhancedSearchGuide = AttributeType('2.5.4.47', name='enhancedSearchGuide', syntax=syntaxes.EnhancedGuide())
-facsimileTelephoneNumber = AttributeType('2.5.4.23', name='facsimileTelephoneNumber', syntax=syntaxes.FacsimileTelephoneNumber())
-generationQualifier = AttributeType('2.5.4.44', name='generationQualifier', sup=name)
-givenName = AttributeType('2.5.4.42', name='givenName', sup=name)
-houseIdentifier = AttributeType('2.5.4.51', name='houseIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-initials = AttributeType('2.5.4.43', name='initials', sup=name)
-internationalISDNNumber = AttributeType('2.5.4.25', name='internationalISDNNumber', equality=matching_rules.numericStringMatch, substr=matching_rules.numericStringSubstringsMatch, syntax=syntaxes.NumericString())
-l = AttributeType('2.5.4.7', name='l', sup=name)
-member = AttributeType('2.5.4.31', name='member', sup=distinguishedName)
-o = AttributeType('2.5.4.10', name='o', sup=name)
-ou = AttributeType('2.5.4.11', name='ou', sup=name)
-owner = AttributeType('2.5.4.32', name='owner', sup=distinguishedName)
-physicalDeliveryOfficeName = AttributeType('2.5.4.19', name='physicalDeliveryOfficeName', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-postalAddress = AttributeType('2.5.4.16', name='postalAddress', equality=matching_rules.caseIgnoreListMatch, substr=matching_rules.caseIgnoreListSubstringsMatch, syntax=syntaxes.PostalAddress())
-postalCode = AttributeType('2.5.4.17', name='postalCode', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-postOfficeBox = AttributeType('2.5.4.18', name='postOfficeBox', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-preferredDeliveryMethod = AttributeType('2.5.4.28', name='preferredDeliveryMethod', syntax=syntaxes.DeliveryMethod(), single_value=True)
-registeredAddress = AttributeType('2.5.4.26', name='registeredAddress', sup=postalAddress, syntax=syntaxes.PostalAddress())
-roleOccupant = AttributeType('2.5.4.33', name='roleOccupant', sup=distinguishedName)
-searchGuide = AttributeType('2.5.4.14', name='searchGuide', syntax=syntaxes.Guide())
-seeAlso = AttributeType('2.5.4.34', name='seeAlso', sup=distinguishedName)
-serialNumber = AttributeType('2.5.4.5', name='serialNumber', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.PrintableString())
-sn = AttributeType('2.5.4.4', name='sn', sup=name)
-st = AttributeType('2.5.4.8', name='st', sup=name)
-street = AttributeType('2.5.4.9', name='street', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-telephoneNumber = AttributeType('2.5.4.20', name='telephoneNumber', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber())
-teletexTerminalIdentifier = AttributeType('2.5.4.22', name='teletexTerminalIdentifier', syntax=syntaxes.TeletexTerminalIdentifier())
-telexNumber = AttributeType('2.5.4.21', name='telexNumber', syntax=syntaxes.TelexNumber())
-title = AttributeType('2.5.4.12', name='title', sup=name)
-uid = AttributeType('0.9.2342.19200300.100.1.1', name='uid', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-uniqueMember = AttributeType('2.5.4.50', name='uniqueMember', equality=matching_rules.uniqueMemberMatch, syntax=syntaxes.NameAndOptionalUID())
-userPassword = AttributeType('2.5.4.35', name='userPassword', equality=matching_rules.octetStringMatch, syntax=syntaxes.OctetString())
-x121Address = AttributeType('2.5.4.24', name='x121Address', equality=matching_rules.numericStringMatch, substr=matching_rules.numericStringSubstringsMatch, syntax=syntaxes.NumericString())
-x500UniqueIdentifier = AttributeType('2.5.4.45', name='x500UniqueIdentifier', equality=matching_rules.bitStringMatch, syntax=syntaxes.BitString())
-
-ALL = ALL + (
-	name,
-	businessCategory,
-	c,
-	cn,
-	dc,
-	description,
-	destinationIndicator,
-	distinguishedName,
-	dnQualifier,
-	enhancedSearchGuide,
-	facsimileTelephoneNumber,
-	generationQualifier,
-	givenName,
-	houseIdentifier,
-	initials,
-	internationalISDNNumber,
-	l,
-	member,
-	o,
-	ou,
-	owner,
-	physicalDeliveryOfficeName,
-	postalAddress,
-	postalCode,
-	postOfficeBox,
-	preferredDeliveryMethod,
-	registeredAddress,
-	roleOccupant,
-	searchGuide,
-	seeAlso,
-	serialNumber,
-	sn,
-	st,
-	street,
-	telephoneNumber,
-	teletexTerminalIdentifier,
-	telexNumber,
-	title,
-	uid,
-	uniqueMember,
-	userPassword,
-	x121Address,
-	x500UniqueIdentifier,
-)
diff --git a/ldapserver/schema/rfc4519/matching_rules.py b/ldapserver/schema/rfc4519/matching_rules.py
deleted file mode 100644
index 1ef1df999613338c3ced480a6bdb1256c03963f8..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4519/matching_rules.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4512.matching_rules import *
diff --git a/ldapserver/schema/rfc4519/object_classes.py b/ldapserver/schema/rfc4519/object_classes.py
deleted file mode 100644
index 412bc590c95868f4fa5889edea28ae0b78166daf..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4519/object_classes.py
+++ /dev/null
@@ -1,38 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import ObjectClass, ObjectClassKind
-from ..rfc4512.object_classes import *
-from . import attribute_types
-
-person = ObjectClass('2.5.6.6', name='person', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.sn, attribute_types.cn], may=[attribute_types.userPassword, attribute_types.telephoneNumber, attribute_types.seeAlso, attribute_types.description]) # defined first, so sup=person works
-applicationProcess = ObjectClass('2.5.6.11', name='applicationProcess', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.seeAlso, attribute_types.ou, attribute_types.l, attribute_types.description])
-country = ObjectClass('2.5.6.2', name='country', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.c], may=[attribute_types.searchGuide, attribute_types.description])
-dcObject = ObjectClass('1.3.6.1.4.1.1466.344', name='dcObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.dc])
-device = ObjectClass('2.5.6.14', name='device', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.serialNumber, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.l, attribute_types.description])
-groupOfNames = ObjectClass('2.5.6.9', name='groupOfNames', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.member, attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description])
-groupOfUniqueNames = ObjectClass('2.5.6.17', name='groupOfUniqueNames', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.uniqueMember, attribute_types.cn], may=[attribute_types.businessCategory, attribute_types.seeAlso, attribute_types.owner, attribute_types.ou, attribute_types.o, attribute_types.description])
-locality = ObjectClass('2.5.6.3', name='locality', sup=top, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.street, attribute_types.seeAlso, attribute_types.searchGuide, attribute_types.st, attribute_types.l, attribute_types.description])
-organization = ObjectClass('2.5.6.4', name='organization', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.o], may=[attribute_types.userPassword, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l, attribute_types.description])
-organizationalPerson = ObjectClass('2.5.6.7', name='organizationalPerson', sup=person, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.title, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.ou, attribute_types.st, attribute_types.l])
-organizationalRole = ObjectClass('2.5.6.8', name='organizationalRole', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.seeAlso, attribute_types.roleOccupant, attribute_types.preferredDeliveryMethod, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.ou, attribute_types.st, attribute_types.l, attribute_types.description])
-organizationalUnit = ObjectClass('2.5.6.5', name='organizationalUnit', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.ou], may=[attribute_types.businessCategory, attribute_types.description, attribute_types.destinationIndicator, attribute_types.facsimileTelephoneNumber, attribute_types.internationalISDNNumber, attribute_types.l, attribute_types.physicalDeliveryOfficeName, attribute_types.postalAddress, attribute_types.postalCode, attribute_types.postOfficeBox, attribute_types.preferredDeliveryMethod, attribute_types.registeredAddress, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.st, attribute_types.street, attribute_types.telephoneNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telexNumber, attribute_types.userPassword, attribute_types.x121Address])
-residentialPerson = ObjectClass('2.5.6.10', name='residentialPerson', sup=person, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.l], may=[attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.preferredDeliveryMethod, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l])
-uidObject = ObjectClass('1.3.6.1.1.3.1', name='uidObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.uid])
-
-ALL = ALL + (
-	person,
-	applicationProcess,
-	country,
-	dcObject,
-	device,
-	groupOfNames,
-	groupOfUniqueNames,
-	locality,
-	organization,
-	organizationalPerson,
-	organizationalRole,
-	organizationalUnit,
-	residentialPerson,
-	uidObject,
-)
diff --git a/ldapserver/schema/rfc4519/syntaxes.py b/ldapserver/schema/rfc4519/syntaxes.py
deleted file mode 100644
index d6e0781b244310e4ab07d3136edf420256c5aee5..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4519/syntaxes.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4512.syntaxes import *
diff --git a/ldapserver/schema/rfc4523/__init__.py b/ldapserver/schema/rfc4523/__init__.py
deleted file mode 100644
index ecb7ec89295fc079b08100f4d211d70118dba022..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4523/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types
diff --git a/ldapserver/schema/rfc4523/attribute_types.py b/ldapserver/schema/rfc4523/attribute_types.py
deleted file mode 100644
index f8585d5ae35dcd269823309db934245e94640487..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4523/attribute_types.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ..types import AttributeType
-from . import syntaxes, matching_rules
-
-userCertificate = AttributeType('2.5.4.36', name='userCertificate', desc='X.509 user certificate', equality=matching_rules.certificateExactMatch, syntax=syntaxes.X509Certificate())
-
-ALL = (
-	userCertificate,
-)
diff --git a/ldapserver/schema/rfc4523/matching_rules.py b/ldapserver/schema/rfc4523/matching_rules.py
deleted file mode 100644
index a556255424018e8bcc6b7998ba3502902733fff2..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4523/matching_rules.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from ..types import MatchingRule
-from . import syntaxes
-
-certificateExactMatch = MatchingRule('2.5.13.34', name='certificateExactMatch', desc='X.509 Certificate Exact Match', syntax=syntaxes.X509CertificateExactAssertion())
-
-ALL = (
-	certificateExactMatch,
-)
diff --git a/ldapserver/schema/rfc4523/syntaxes.py b/ldapserver/schema/rfc4523/syntaxes.py
deleted file mode 100644
index 851edc685ca7775b1fab98aa7ec87e4c33ac998c..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4523/syntaxes.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from ..rfc4517.syntaxes import BytesSyntax
-
-class X509Certificate(BytesSyntax):
-	oid = '1.3.6.1.4.1.1466.115.121.1.8'
-	desc = 'X.509 Certificate'
-
-class X509CertificateExactAssertion(BytesSyntax):
-	oid = '1.3.6.1.1.15.1'
-	desc = 'X.509 Certificate Exact Assertion'
-
-ALL = (
-	X509Certificate,
-	X509CertificateExactAssertion,
-)
diff --git a/ldapserver/schema/rfc4524/__init__.py b/ldapserver/schema/rfc4524/__init__.py
deleted file mode 100644
index 1727734f9ec5518c83e74fb73658a6a8325b070f..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4524/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import syntaxes, matching_rules, attribute_types, object_classes
diff --git a/ldapserver/schema/rfc4524/attribute_types.py b/ldapserver/schema/rfc4524/attribute_types.py
deleted file mode 100644
index bdd2c687fff69b9f2dfc58ae99c95555964c1f4a..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4524/attribute_types.py
+++ /dev/null
@@ -1,60 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import AttributeType
-from ..rfc4519.attribute_types import *
-from . import syntaxes, matching_rules
-
-associatedDomain = AttributeType('0.9.2342.19200300.100.1.37', name='associatedDomain', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String())
-associatedName = AttributeType('0.9.2342.19200300.100.1.38', name='associatedName', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN())
-buildingName = AttributeType('0.9.2342.19200300.100.1.48', name='buildingName', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-co = AttributeType('0.9.2342.19200300.100.1.43', name='co', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-documentAuthor = AttributeType('0.9.2342.19200300.100.1.14', name='documentAuthor', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN())
-documentIdentifier = AttributeType('0.9.2342.19200300.100.1.11', name='documentIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-documentLocation = AttributeType('0.9.2342.19200300.100.1.15', name='documentLocation', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-documentPublisher = AttributeType('0.9.2342.19200300.100.1.56', name='documentPublisher', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString())
-documentTitle = AttributeType('0.9.2342.19200300.100.1.12', name='documentTitle', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-documentVersion = AttributeType('0.9.2342.19200300.100.1.13', name='documentVersion', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-drink = AttributeType('0.9.2342.19200300.100.1.5', name='drink', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-homePhone = AttributeType('0.9.2342.19200300.100.1.20', name='homePhone', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber())
-homePostalAddress = AttributeType('0.9.2342.19200300.100.1.39', name='homePostalAddress', equality=matching_rules.caseIgnoreListMatch, substr=matching_rules.caseIgnoreListSubstringsMatch, syntax=syntaxes.PostalAddress())
-host = AttributeType('0.9.2342.19200300.100.1.9', name='host', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-info = AttributeType('0.9.2342.19200300.100.1.4', name='info', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(2048))
-mail = AttributeType('0.9.2342.19200300.100.1.3', name='mail', equality=matching_rules.caseIgnoreIA5Match, substr=matching_rules.caseIgnoreIA5SubstringsMatch, syntax=syntaxes.IA5String(256))
-manager = AttributeType('0.9.2342.19200300.100.1.10', name='manager', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN())
-mobile = AttributeType('0.9.2342.19200300.100.1.41', name='mobile', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber())
-organizationalStatus = AttributeType('0.9.2342.19200300.100.1.45', name='organizationalStatus', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-pager = AttributeType('0.9.2342.19200300.100.1.42', name='pager', equality=matching_rules.telephoneNumberMatch, substr=matching_rules.telephoneNumberSubstringsMatch, syntax=syntaxes.TelephoneNumber())
-personalTitle = AttributeType('0.9.2342.19200300.100.1.40', name='personalTitle', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-roomNumber = AttributeType('0.9.2342.19200300.100.1.6', name='roomNumber', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-secretary = AttributeType('0.9.2342.19200300.100.1.21', name='secretary', equality=matching_rules.distinguishedNameMatch, syntax=syntaxes.DN())
-uniqueIdentifier = AttributeType('0.9.2342.19200300.100.1.44', name='uniqueIdentifier', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-UserClass = AttributeType('0.9.2342.19200300.100.1.8', name='userClass', equality=matching_rules.caseIgnoreMatch, substr=matching_rules.caseIgnoreSubstringsMatch, syntax=syntaxes.DirectoryString(256))
-
-ALL = ALL + (
-	associatedDomain,
-	associatedName,
-	buildingName,
-	co,
-	documentAuthor,
-	documentIdentifier,
-	documentLocation,
-	documentPublisher,
-	documentTitle,
-	documentVersion,
-	drink,
-	homePhone,
-	homePostalAddress,
-	host,
-	info,
-	mail,
-	manager,
-	mobile,
-	organizationalStatus,
-	pager,
-	personalTitle,
-	roomNumber,
-	secretary,
-	uniqueIdentifier,
-	UserClass,
-)
diff --git a/ldapserver/schema/rfc4524/matching_rules.py b/ldapserver/schema/rfc4524/matching_rules.py
deleted file mode 100644
index e3a7ec210d1eafac6d3748cb9abd780d71ea094c..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4524/matching_rules.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4519.matching_rules import *
diff --git a/ldapserver/schema/rfc4524/object_classes.py b/ldapserver/schema/rfc4524/object_classes.py
deleted file mode 100644
index 584a0cf267cfebd0bf89a63e62fcbc617f520a99..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4524/object_classes.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..types import ObjectClass, ObjectClassKind
-from ..rfc4519.object_classes import *
-from . import attribute_types
-
-account = ObjectClass('0.9.2342.19200300.100.4.5', name='account', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.uid], may=[attribute_types.description, attribute_types.seeAlso, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.host] )
-document = ObjectClass('0.9.2342.19200300.100.4.6', name='document', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.documentIdentifier], may=[attribute_types.cn, attribute_types.description, attribute_types.seeAlso, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.documentTitle, attribute_types.documentVersion, attribute_types.documentAuthor, attribute_types.documentLocation, attribute_types.documentPublisher] )
-documentSeries = ObjectClass('0.9.2342.19200300.100.4.9', name='documentSeries', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.description, attribute_types.l, attribute_types.o, attribute_types.ou, attribute_types.seeAlso, attribute_types.telephoneNumber] )
-domain = ObjectClass('0.9.2342.19200300.100.4.13', name='domain', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.dc], may=[attribute_types.userPassword, attribute_types.searchGuide, attribute_types.seeAlso, attribute_types.businessCategory, attribute_types.x121Address, attribute_types.registeredAddress, attribute_types.destinationIndicator, attribute_types.preferredDeliveryMethod, attribute_types.telexNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telephoneNumber, attribute_types.internationalISDNNumber, attribute_types.facsimileTelephoneNumber, attribute_types.street, attribute_types.postOfficeBox, attribute_types.postalCode, attribute_types.postalAddress, attribute_types.physicalDeliveryOfficeName, attribute_types.st, attribute_types.l, attribute_types.description, attribute_types.o, attribute_types.associatedName] )
-domainRelatedObject = ObjectClass('0.9.2342.19200300.100.4.17', name='domainRelatedObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.associatedDomain])
-friendlyCountry = ObjectClass('0.9.2342.19200300.100.4.18', name='friendlyCountry', sup=country, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.co])
-rFC822localPart = ObjectClass('0.9.2342.19200300.100.4.14', name='rFC822localPart', sup=domain, kind=ObjectClassKind.STRUCTURAL, may=[attribute_types.cn, attribute_types.description, attribute_types.destinationIndicator, attribute_types.facsimileTelephoneNumber, attribute_types.internationalISDNNumber, attribute_types.physicalDeliveryOfficeName, attribute_types.postalAddress, attribute_types.postalCode, attribute_types.postOfficeBox, attribute_types.preferredDeliveryMethod, attribute_types.registeredAddress, attribute_types.seeAlso, attribute_types.sn, attribute_types.street, attribute_types.telephoneNumber, attribute_types.teletexTerminalIdentifier, attribute_types.telexNumber, attribute_types.x121Address] )
-room = ObjectClass('0.9.2342.19200300.100.4.7', name='room', sup=top, kind=ObjectClassKind.STRUCTURAL, must=[attribute_types.cn], may=[attribute_types.roomNumber, attribute_types.description, attribute_types.seeAlso, attribute_types.telephoneNumber] )
-simpleSecurityObject = ObjectClass('0.9.2342.19200300.100.4.19', name='simpleSecurityObject', sup=top, kind=ObjectClassKind.AUXILIARY, must=[attribute_types.userPassword])
-
-ALL = ALL + (
-	account,
-	document,
-	documentSeries,
-	domain,
-	domainRelatedObject,
-	friendlyCountry,
-	rFC822localPart,
-	room,
-	simpleSecurityObject,
-)
diff --git a/ldapserver/schema/rfc4524/syntaxes.py b/ldapserver/schema/rfc4524/syntaxes.py
deleted file mode 100644
index c26e86963cec30db1c1c0f9acb9aed14b667cc3d..0000000000000000000000000000000000000000
--- a/ldapserver/schema/rfc4524/syntaxes.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-# pylint: disable=wildcard-import,unused-wildcard-import
-
-from ..rfc4519.syntaxes import *
diff --git a/ldapserver/schema/syntaxes.py b/ldapserver/schema/syntaxes.py
new file mode 100644
index 0000000000000000000000000000000000000000..a17e1a29155327f43c161d01266d0ade65c974fe
--- /dev/null
+++ b/ldapserver/schema/syntaxes.py
@@ -0,0 +1,356 @@
+import re
+import datetime
+
+from .definitions import SyntaxDefinition
+from .. import dn, exceptions
+
+class BytesSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		return value
+
+	def decode(self, schema, raw_value):
+		return raw_value
+
+class StringSyntaxDefinition(SyntaxDefinition):
+	def __init__(self, oid, encoding='utf-8', re_pattern='.*', **kwargs):
+		super().__init__(oid, **kwargs)
+		self.encoding = encoding
+		self.re_pattern = re.compile(re_pattern)
+
+	def encode(self, schema, value):
+		return value.encode(self.encoding)
+
+	def decode(self, schema, raw_value):
+		try:
+			value = raw_value.decode(self.encoding)
+		except UnicodeDecodeError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax(exc.reason) from exc
+		if not re.fullmatch(self.re_pattern, value):
+			raise exceptions.LDAPInvalidAttributeSyntax()
+		return value
+
+class IntegerSyntaxDefinition(StringSyntaxDefinition):
+	def __init__(self, oid, **kwargs):
+		super().__init__(oid, encoding='ascii', re_pattern='([0-9]|-?[1-9][0-9]+)', **kwargs)
+
+	def encode(self, schema, value):
+		return super().encode(schema, str(value))
+
+	def decode(self, schema, raw_value):
+		try:
+			return int(super().decode(schema, raw_value))
+		except ValueError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax() from exc
+
+class SchemaElementSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		return str(value).encode('utf8')
+
+class BooleanSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		return b'TRUE' if value else b'FALSE'
+
+	def decode(self, schema, raw_value):
+		if raw_value == b'TRUE':
+			return True
+		elif raw_value == b'FALSE':
+			return False
+		else:
+			raise exceptions.LDAPInvalidAttributeSyntax()
+
+class DNSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		return str(value).encode('utf8')
+
+	def decode(self, schema, raw_value):
+		try:
+			return dn.DN.from_str(raw_value.decode('utf8'))
+		except (UnicodeDecodeError, TypeError, ValueError) as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax() from exc
+
+class NameAndOptionalUIDSyntaxDefinition(StringSyntaxDefinition):
+	def encode(self, schema, value):
+		return str(value).encode('utf8')
+
+	def decode(self, schema, raw_value):
+		try:
+			return dn.DNWithUID.from_str(raw_value.decode('utf8'))
+		except (UnicodeDecodeError, TypeError, ValueError) as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax() from exc
+
+class GeneralizedTimeSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		if value.microsecond:
+			str_value = value.strftime('%Y%m%d%H%M%S.%f')
+		elif value.second:
+			str_value = value.strftime('%Y%m%d%H%M%S')
+		else:
+			str_value = value.strftime('%Y%m%d%H%M')
+		if value.tzinfo == datetime.timezone.utc:
+			str_value += 'Z'
+		elif value.tzinfo is not None:
+			delta_seconds = value.tzinfo.utcoffset(value).total_seconds()
+			if delta_seconds < 0:
+				str_value += '-'
+				delta_seconds = -delta_seconds
+			else:
+				str_value += '+'
+			hour = delta_seconds // 3600
+			minute = (delta_seconds % 3600) // 60
+			str_value += '%02d%02d'%(hour, minute)
+		return str_value.encode('ascii')
+
+	def decode(self, schema, raw_value):
+		try:
+			raw_value = raw_value.decode('utf8')
+		except UnicodeDecodeError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax() from exc
+		match = re.fullmatch(r'([0-9]{10})(|[0-9]{2}|[0-9]{4})(|[,.][0-9]+)(Z|[+-][0-9]{2}|[+-][0-9]{4})', raw_value)
+		if match is None:
+			raise exceptions.LDAPInvalidAttributeSyntax()
+		main, minute_second, fraction, timezone = match.groups()
+		fraction = float('0.' + (fraction[1:] or '0'))
+		result = datetime.datetime.strptime(main, '%Y%m%d%H')
+		if not minute_second:
+			result += datetime.timedelta(hours=fraction)
+		if len(minute_second) == 2:
+			result += datetime.timedelta(minutes=int(minute_second)+fraction)
+		elif len(minute_second) == 4:
+			minute = minute_second[:2]
+			second = minute_second[2:4]
+			result += datetime.timedelta(minutes=int(minute), seconds=int(second)+fraction)
+		if timezone == 'Z':
+			result = result.replace(tzinfo=datetime.timezone.utc)
+		elif timezone:
+			sign, hour, minute = timezone[0], timezone[1:3], (timezone[3:5] or '00')
+			delta = datetime.timedelta(hours=int(hour), minutes=int(minute))
+			if sign == '+':
+				result = result.replace(tzinfo=datetime.timezone(delta))
+			else:
+				result = result.replace(tzinfo=datetime.timezone(-delta))
+		return result
+
+class PostalAddressSyntaxDefinition(SyntaxDefinition):
+	# 3.3.28.  Postal Address
+	#
+	# A value of the Postal Address syntax is a sequence of strings of one
+	# or more arbitrary UCS characters, which form an address in a physical
+	# mail system.
+	#
+	# The LDAP-specific encoding of a value of this syntax is defined by
+	# the following ABNF:
+	#
+	#
+	#   PostalAddress = line *( DOLLAR line )
+	#   line          = 1*line-char
+	#   line-char     = %x00-23
+	#                   / (%x5C "24")  ; escaped "$"
+	#                   / %x25-5B
+	#                   / (%x5C "5C")  ; escaped "\"
+	#                   / %x5D-7F
+	#                   / UTFMB
+	#
+	# Each character string (i.e., <line>) of a postal address value is
+	# encoded as a UTF-8 [RFC3629] string, except that "\" and "$"
+	# characters, if they occur in the string, are escaped by a "\"
+	# character followed by the two hexadecimal digit code for the
+	# character.  The <DOLLAR> and <UTFMB> rules are defined in [RFC4512].
+	#
+	# Many servers limit the postal address to no more than six lines of no
+	# more than thirty characters each.
+	#
+	#   Example:
+	#      1234 Main St.$Anytown, CA 12345$USA
+	#      \241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA
+	#
+	# The LDAP definition for the Postal Address syntax is:
+	#
+	#   ( 1.3.6.1.4.1.1466.115.121.1.41 DESC 'Postal Address' )
+	#
+	# This syntax corresponds to the PostalAddress ASN.1 type from [X.520];
+	# that is
+	#
+	#   PostalAddress ::= SEQUENCE SIZE(1..ub-postal-line) OF
+	#       DirectoryString { ub-postal-string }
+	#
+	# The values of ub-postal-line and ub-postal-string (both integers) are
+	# implementation defined.  Non-normative definitions appear in [X.520].
+
+	# Native values are lists of str
+	def encode(self, schema, value):
+		return '$'.join([line.replace('\\', '\\5C').replace('$', '\\24') for line in value]).encode('utf8')
+
+	def decode(self, schema, raw_value):
+		return [line.replace('\\24', '$').replace('\\5C', '\\') for line in raw_value.decode('utf8').split('$')]
+
+class SubstringAssertionSyntaxDefinition(SyntaxDefinition):
+	# Native values are lists of str
+	def encode(self, schema, value):
+		raise NotImplementedError()
+
+	def decode(self, schema, raw_value):
+		value = raw_value.decode('utf8')
+		if '*' not in value:
+			raise exceptions.LDAPInvalidAttributeSyntax()
+		substrings = [substring.replace('\\2A', '*').replace('\\5C', '\\') for substring in value.split('*')]
+		initial_substring, *any_substring, final_substring = substrings
+		return (initial_substring or None, any_substring, final_substring or None)
+
+class UTCTimeSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		if value.second:
+			str_value = value.strftime('%y%m%d%H%M%S')
+		else:
+			str_value = value.strftime('%y%m%d%H%M')
+		if value.tzinfo == datetime.timezone.utc:
+			str_value += 'Z'
+		elif value.tzinfo is not None:
+			delta_seconds = value.tzinfo.utcoffset(value).total_seconds()
+			if delta_seconds < 0:
+				str_value += '-'
+				delta_seconds = -delta_seconds
+			else:
+				str_value += '+'
+			hour = delta_seconds // 3600
+			minute = (delta_seconds % 3600) // 60
+			str_value += '%02d%02d'%(hour, minute)
+		return str_value.encode('ascii')
+
+	def decode(self, schema, raw_value):
+		try:
+			raw_value = raw_value.decode('utf8')
+		except UnicodeDecodeError as exc:
+			raise exceptions.LDAPInvalidAttributeSyntax() from exc
+		match = re.fullmatch(r'([0-9]{10})(|[0-9]{2})(|Z|[+-][0-9]{4})', raw_value)
+		if match is None:
+			raise exceptions.LDAPInvalidAttributeSyntax()
+		main, seconds, timezone = match.groups()
+		result = datetime.datetime.strptime(main, '%y%m%d%H%M')
+		if seconds:
+			result = result.replace(second=int(seconds))
+		if timezone == 'Z':
+			result = result.replace(tzinfo=datetime.timezone.utc)
+		elif timezone:
+			sign, hour, minute = timezone[0], timezone[1:3], timezone[3:5]
+			delta = datetime.timedelta(hours=int(hour), minutes=int(minute))
+			if sign == '+':
+				result = result.replace(tzinfo=datetime.timezone(delta))
+			else:
+				result = result.replace(tzinfo=datetime.timezone(-delta))
+		return result
+
+class StubSyntaxDefinition(SyntaxDefinition):
+	def encode(self, schema, value):
+		raise NotImplementedError()
+
+# RFC2252 (deprecated legacy syntaxes required for some schemas)
+Binary =  BytesSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.5', desc='Binary')
+
+# RFC4517
+Fax = BytesSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.23', desc='Fax')
+JPEG = BytesSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.28', desc='JPEG')
+OctetString = BytesSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.40', desc='Octet String')
+DirectoryString = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.15', desc='Directory String')
+IA5String = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.26', desc='IA5 String', encoding='ascii', extra_compatability_tags=DirectoryString.compatability_tags)
+PrintableString = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.44', desc='Printable String', encoding='ascii', re_pattern='[A-Za-z0-9\'()+,.=/:? -]*', extra_compatability_tags=IA5String.compatability_tags)
+CountryString = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.11', desc='Country String', encoding='ascii', re_pattern='[A-Za-z0-9\'()+,.=/:? -]{2}', extra_compatability_tags=PrintableString.compatability_tags)
+TelephoneNumber = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.50', desc='Telephone Number', encoding='ascii', re_pattern='[A-Za-z0-9\'()+,.=/:? -]*', extra_compatability_tags=PrintableString.compatability_tags)
+OID = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.38', desc='OID', encoding='ascii', re_pattern=r'[0-9]+(\.[0-9])*|[A-Za-z][A-Za-z0-9-]*')
+BitString = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.6', desc='Bit String', encoding='ascii', re_pattern='\'[01]*\'B')
+DeliveryMethod = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.14', desc='Delivery Method')
+FacsimileTelephoneNumber = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.22', desc='Facsimile Telephone Number')
+EnhancedGuide = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.21', desc='Enhanced Guide')
+Guide = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.25', desc='Guide')
+NumericString = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.36', desc='Numeric String', encoding='ascii', re_pattern='[0-9 ]+')
+OtherMailbox = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.39', desc='Other Mailbox')
+TeletexTerminalIdentifier = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.51', desc='Teletex Terminal Identifier')
+TelexNumber = StringSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.52', desc='Telex Number')
+INTEGER = IntegerSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.27', desc='INTEGER')
+AttributeTypeDescription = SchemaElementSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.3', desc='Attribute Type Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+LDAPSyntaxDescription = SchemaElementSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.54', desc='LDAP Syntax Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+MatchingRuleDescription = SchemaElementSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.30', desc='Matching Rule Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+MatchingRuleUseDescription = SchemaElementSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.31', desc='Matching Rule Use Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+ObjectClassDescription = SchemaElementSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.37', desc='Object Class Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+Boolean = BooleanSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.7', desc='Boolean')
+DN = DNSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.12', desc='DN')
+NameAndOptionalUID = NameAndOptionalUIDSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.34', desc='Name And Optional UID')
+GeneralizedTime = GeneralizedTimeSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.24', desc='Generalized Time')
+PostalAddress = PostalAddressSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.41', desc='Postal Address')
+SubstringAssertion = SubstringAssertionSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.58', desc='Substring Assertion')
+UTCTime = UTCTimeSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.53', desc='UTC Time')
+DITContentRuleDescription = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.16', desc='DIT Content Rule Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+DITStructureRuleDescription = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.17', desc='DIT Structure Rule Description', extra_compatability_tags=['FirstComponent:'+INTEGER.oid])
+NameFormDescription = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.35', desc='Name Form Description', extra_compatability_tags=['FirstComponent:'+OID.oid])
+
+# RFC4523
+X509Certificate = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.8', desc='X.509 Certificate')
+X509CertificateList = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.9', desc='X.509 Certificate List')
+X509CertificatePair = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.10', desc='X.509 Certificate Pair')
+X509SupportedAlgorithm = StubSyntaxDefinition('1.3.6.1.4.1.1466.115.121.1.49', desc='X.509 Supported Algorithm')
+X509CertificateExactAssertion = StubSyntaxDefinition('1.3.6.1.1.15.1', desc='X.509 Certificate Exact Assertion')
+X509CertificateAssertion = StubSyntaxDefinition('1.3.6.1.1.15.2', desc='X.509 Certificate Assertion')
+X509CertificatePairExactAssertion = StubSyntaxDefinition('1.3.6.1.1.15.3', desc='X.509 Certificate Pair Exact Assertion')
+X509CertificatePairAssertion = StubSyntaxDefinition('1.3.6.1.1.15.4', desc='X.509 Certificate Pair Assertion')
+X509CertificateListExactAssertion = StubSyntaxDefinition('1.3.6.1.1.15.5', desc='X.509 Certificate List Exact Assertion')
+X509CertificateListAssertion = StubSyntaxDefinition('1.3.6.1.1.15.6', desc='X.509 Certificate List Assertion')
+X509AlgorithmIdentifier = StubSyntaxDefinition('1.3.6.1.1.15.7', desc='X.509 Algorithm Identifier')
+
+# RFC3112
+AuthPasswordSyntax = StubSyntaxDefinition('1.3.6.1.4.1.4203.1.1.2', desc='authentication password syntax')
+
+ALL = (
+	# RFC2252
+	Binary,
+
+	# RFC4517
+	AttributeTypeDescription,
+	BitString,
+	Boolean,
+	CountryString,
+	DeliveryMethod,
+	DirectoryString,
+	DITContentRuleDescription,
+	DITStructureRuleDescription,
+	DN,
+	EnhancedGuide,
+	FacsimileTelephoneNumber,
+	Fax,
+	GeneralizedTime,
+	Guide,
+	IA5String,
+	INTEGER,
+	JPEG,
+	LDAPSyntaxDescription,
+	MatchingRuleDescription,
+	MatchingRuleUseDescription,
+	NameAndOptionalUID,
+	NameFormDescription,
+	NumericString,
+	ObjectClassDescription,
+	OctetString,
+	OID,
+	OtherMailbox,
+	PostalAddress,
+	PrintableString,
+	SubstringAssertion,
+	TelephoneNumber,
+	TeletexTerminalIdentifier,
+	TelexNumber,
+	UTCTime,
+
+	# RFC4523
+	X509Certificate,
+	X509CertificateList,
+	X509CertificatePair,
+	X509SupportedAlgorithm,
+	X509CertificateExactAssertion,
+	X509CertificateAssertion,
+	X509CertificatePairExactAssertion,
+	X509CertificatePairAssertion,
+	X509CertificateListExactAssertion,
+	X509CertificateListAssertion,
+	X509AlgorithmIdentifier,
+
+	# RFC3112
+	AuthPasswordSyntax,
+)
diff --git a/ldapserver/schema/types.py b/ldapserver/schema/types.py
index 3af0c061fa68f8a82ca4043ba4a8efa1a9bfbb09..c5f24df9aa56010db5d8025d55e8c597598b667f 100644
--- a/ldapserver/schema/types.py
+++ b/ldapserver/schema/types.py
@@ -1,452 +1,383 @@
-import enum
-import re
-
-def escape(string):
-	result = ''
-	for char in string:
-		if char == '\'':
-			result += '\\27'
-		elif char == '\\':
-			result += '\\5C'
-		else:
-			result += char
-	return result
+# pylint: disable=too-many-instance-attributes,too-many-arguments,too-many-locals
 
-class Syntax:
-	'''LDAP syntax for attribute and assertion values
-
-	Instances of the class represent (optionally length-constrained) syntax
-	refercences as used with `MatchingRule` and `AttributeType`.'''
-	oid: str
-	desc: str
-
-	def __init__(self, max_len=None):
-		self.max_len = max_len
-		if max_len is None:
-			self.ref = self.oid
-		else:
-			self.ref = self.oid + '{' + str(max_len) + '}'
-
-	@classmethod
-	def get_first_component_oid(cls):
-		'''Used by objectIdentifierFirstComponentMatch'''
-		return cls.oid
-
-	@classmethod
-	def to_definition(cls):
-		'''Get string reperesentation as used in ldapSyntaxes attribute'''
-		return f"( {cls.oid} DESC '{escape(cls.desc)}' )"
-
-	def decode(self, schema, raw_value):
-		'''Decode LDAP-specific encoding of a value to a native value
-
-		:param schema: Schema of the object in whose context decoding takes place
-		:type schema: Schema
-		:param raw_value: LDAP-specific encoding of the value
-		:type raw_value: bytes
-
-		:returns: native value (depends on syntax), None if raw_value is invalid
-		:rtype: any or None'''
-		return None
-
-	def encode(self, schema, value):
-		'''Encode native value to its LDAP-specific encoding
-
-		:param schema: Schema of the object in whose context encoding takes place
-		:type schema: Schema
-		:param value: native value (depends on syntax)
-		:type value: any
-
-		:returns: LDAP-specific encoding of the value
-		:rtype: bytes'''
-		raise NotImplementedError()
+import collections.abc
 
-class MatchingRule:
-	'''Matching rules define how to compare attribute with assertion values
-
-	Instances provide a number of methods to compare a single attribute value
-	to an assertion value. Attribute values are passed as the native type of
-	the attribute type's syntax. Assertion values are always decoded with the
-	matching rule's syntax before being passed to a matching method.
-
-	LDAP differenciates between EQUALITY, SUBSTR and ORDERING matching rules.
-	This class is used for all of them. Inappropriate matching methods should
-	always return None. '''
-	def __init__(self, oid, name, syntax, **kwargs):
-		self.oid = oid
-		self.name = name
-		self.names = [name]
-		self.syntax = syntax
-		for key, value in kwargs.items():
-			setattr(self, key, value)
-
-	def to_definition(self):
-		'''Get string reperesentation as used in matchingRules attribute'''
-		return f"( {self.oid} NAME '{escape(self.name)}' SYNTAX {self.syntax.ref} )"
-
-	def get_first_component_oid(self):
-		'''Used by objectIdentifierFirstComponentMatch'''
-		return self.oid
+from .definitions import MatchingRuleKind, AttributeTypeUsage, MatchingRuleUseDefinition, AttributeTypeDefinition, ObjectClassDefinition
+from .. import exceptions
 
-	def __repr__(self):
-		return f'<ldapserver.schema.MatchingRule {self.oid}>'
+__all__ = [
+	'Syntax',
+	'EqualityMatchingRule',
+	'OrderingMatchingRule',
+	'SubstrMatchingRule',
+	'AttributeType',
+	'ObjectClass',
+	'Schema',
+]
 
-	def match_equal(self, schema, attribute_value, assertion_value):
-		'''Return whether attribute value is equal to assertion values
+class Syntax:
+	'''LDAP syntax for attribute and assertion values'''
+	def __init__(self, schema, definition, syntaxes_by_tag):
+		self.schema = schema
+		self.definition = definition
+		self.oid = definition.oid
+		self.ref = self.oid
+		schema._register(self, self.oid, self.ref)
+		schema.syntaxes._register(self, self.oid, self.ref)
+		for tag in definition.compatability_tags:
+			syntaxes_by_tag.setdefault(tag, set()).add(self)
+		# Populated by MatchingRule.__init__
+		self.compatible_matching_rules = set()
 
-		Only available for EQUALITY matching rules.
+	def __repr__(self):
+		return f'<ldapserver.schema.Syntax {self.oid}>'
 
-		:returns: True if attribute value matches the assertion value, False if it
-		          does not match. If the result is Undefined, None is returned.'''
-		return None
+	def encode(self, value):
+		return self.definition.encode(self.schema, value)
 
-	def match_approx(self, schema, attribute_value, assertion_value):
-		'''Return whether attribute value is approximatly equal to assertion values
+	def decode(self, raw_value):
+		return self.definition.decode(self.schema, raw_value)
 
-		Only available for EQUALITY matching rules.
+class MatchingRule:
+	def __init__(self, schema, definition, syntaxes_by_tag):
+		self.schema = schema
+		self.definition = definition
+		self.oid = definition.oid
+		self.syntax = schema.syntaxes[definition.syntax]
+		self.names = self.definition.name
+		self.ref = self.names[0] if self.names else self.oid
+		schema._register(self, self.oid, self.ref, *self.names)
+		schema.matching_rules._register(self, self.oid, self.ref, *self.names)
+		self.compatible_syntaxes = syntaxes_by_tag.setdefault(definition.compatability_tag, set())
+		for syntax in self.compatible_syntaxes:
+			syntax.compatible_matching_rules.add(self)
+		# Populated by AttributeType.__init__
+		self.compatible_attribute_types = set()
+
+	def match_extensible(self, attribute_values, assertion_value):
+		raise exceptions.LDAPInappropriateMatching()
+
+class EqualityMatchingRule(MatchingRule):
+	def __repr__(self):
+		return f'<ldapserver.schema.EqualityMatchingRule {self.ref}>'
 
-		:returns: True if attribute value matches the assertion value, False if it
-		          does not match. If the result is Undefined, None is returned.'''
-		return self.match_equal(schema, attribute_value, assertion_value)
+	def match_extensible(self, attribute_values, assertion_value):
+		return self.definition.match_equal(self.schema, attribute_values, assertion_value)
 
-	def match_less(self, schema, attribute_value, assertion_value):
-		'''Return whether attribute value is less than assertion values
+	def match_equal(self, attribute_values, assertion_value):
+		return self.definition.match_equal(self.schema, attribute_values, assertion_value)
 
-		Only available for ORDERING matching rules.
+	def match_approx(self, attribute_values, assertion_value):
+		return self.definition.match_approx(self.schema, attribute_values, assertion_value)
 
-		:returns: True if attribute value matches the assertion value, False if it
-		          does not match. If the result is Undefined, None is returned.'''
-		return None
+class OrderingMatchingRule(MatchingRule):
+	def __repr__(self):
+		return f'<ldapserver.schema.OrderingMatchingRule {self.ref}>'
 
-	def match_greater_or_equal(self, schema, attribute_value, assertion_value):
-		'''Return whether attribute value is greater than or equal to assertion values
+	def match_less(self, attribute_values, assertion_value):
+		return self.definition.match_less(self.schema, attribute_values, assertion_value)
 
-		Only available for ORDERING matching rules.
+	def match_greater_or_equal(self, attribute_values, assertion_value):
+		return self.definition.match_greater_or_equal(self.schema, attribute_values, assertion_value)
 
-		:returns: True if attribute value matches the assertion value, False if it
-		          does not match. If the result is Undefined, None is returned.'''
-		return None
+class SubstrMatchingRule(MatchingRule):
+	def __repr__(self):
+		return f'<ldapserver.schema.SubstrMatchingRule {self.ref}>'
 
-	def match_substr(self, schema, attribute_value, inital_substring, any_substrings, final_substring):
-		'''Return whether attribute value matches substring assertion
+	def match_extensible(self, attribute_values, assertion_value):
+		return self.definition.match_substr(self.schema, attribute_values, assertion_value[0], assertion_value[1], assertion_value[2])
 
-		Only available for SUBSTR matching rules.
+	def match_substr(self, attribute_values, inital_substring, any_substrings, final_substring):
+		return self.definition.match_substr(self.schema, attribute_values, inital_substring, any_substrings, final_substring)
 
-		The type of `inital_substring`, `any_substrings` and `final_substring`
-		depends on the syntax of the attribute's equality matching rule!
+class AttributeType:
+	def __init__(self, schema, definition):
+		self.schema = schema
+		self.definition = definition
+		self.oid = definition.oid
+		self.names = definition.name or []
+		self.ref = self.names[0] if self.names else self.oid
+		self.sup = schema.attribute_types[definition.sup] if definition.sup else None
+		self.subtypes = set()
+		sup = self.sup
+		while sup:
+			self.sup.subtypes.add(self)
+			sup = sup.sup
+		self.equality = schema.matching_rules[definition.equality] if definition.equality else None
+		self.ordering = schema.matching_rules[definition.ordering] if definition.ordering else None
+		self.substr = schema.matching_rules[definition.substr] if definition.substr else None
+		if self.sup:
+			self.equality = self.equality or self.sup.equality
+			self.ordering = self.ordering or self.sup.ordering
+			self.substr = self.substr or self.sup.substr
+		self.syntax = schema.syntaxes[definition.syntax] if definition.syntax else self.sup.syntax
+		self.is_operational = (definition.usage != AttributeTypeUsage.userApplications)
+		schema._register(self, self.oid, self.ref, *self.names)
+		schema.attribute_types._register(self, self.oid, self.ref, *self.names)
+		if not self.is_operational:
+			schema.user_attribute_types.add(self)
+		self.compatible_matching_rules = self.syntax.compatible_matching_rules
+		for matching_rule in self.compatible_matching_rules:
+			matching_rule.compatible_attribute_types.add(self)
 
-		:param schema: Schema of the object whose attribute value is matched
-		:type schema: Schema
-		:param attribute_value: Attribute value (type according to attribute's syntax)
-		:type attribute_value: any
+	def __repr__(self):
+		return f'<ldapserver.schema.AttributeType {self.ref}>'
+
+	def encode(self, value):
+		return self.syntax.encode(value)
+
+	def decode(self, raw_value):
+		return self.syntax.decode(raw_value)
+
+	def match_equal(self, attribute_values, assertion_value):
+		'''Return whether any attribute value equals assertion value
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
+		:param assertion_value: Assertion value
+		:type assertion_value: bytes
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		if self.equality is None:
+			raise exceptions.LDAPInappropriateMatching()
+		assertion_value = self.equality.syntax.decode(assertion_value)
+		return self.equality.match_equal(attribute_values, assertion_value)
+
+	def match_substr(self, attribute_values, inital_substring, any_substrings, final_substring):
+		'''Return whether any attribute value matches a substring assertion
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
 		:param inital_substring: Substring to match the beginning (optional)
-		:type inital_substring: any
+		:type inital_substring: bytes or None
 		:param any_substrings: List of substrings to match between initial and final in order
-		:type any_substrings: list of any
+		:type any_substrings: list of bytes
 		:param final_substring: Substring to match the end (optional)
-		:type final_substring: any
-		:returns: True if attribute value matches the assertion value, False if it
-		          does not match. If the result is Undefined, None is returned.'''
-		return None
-
-class AttributeTypeUsage(enum.Enum):
-	'''Values for usage argument of `AttributeTypeUsage`'''
-	# pylint: disable=invalid-name
-	# user
-	userApplications = enum.auto()
-	# directory operational
-	directoryOperation = enum.auto()
-	# DSA-shared operational
-	distributedOperation = enum.auto()
-	# DSA-specific operational
-	dSAOperation = enum.auto()
-
-class AttributeType:
-	'''Represents an attribute type within a schema'''
-	# pylint: disable=too-many-instance-attributes,too-many-arguments,too-many-branches,too-many-statements
-	def __init__(self, oid, name=None, desc=None, obsolete=None, sup=None,
-	             equality=None, ordering=None, substr=None, syntax=None,
-               single_value=None, collective=None, no_user_modification=None,
-               usage=None):
-		if sup is None and syntax is None:
-			raise ValueError('Either SUP or, syntax=syntax.must, be specified')
-		tokens = ['(', oid]
-		if name is not None:
-			tokens += ['NAME', "'"+escape(name)+"'"] # name is actually a list
-		if desc is not None:
-			tokens += ['DESC', "'"+escape(desc)+"'"]
-		if obsolete is not None:
-			tokens += ['OBSOLETE', obsolete]
-		if sup is not None:
-			tokens += ['SUP', sup.oid]
-		if equality is not None:
-			tokens += ['EQUALITY', equality.oid]
-		if ordering is not None:
-			tokens += ['ORDERING', ordering.oid]
-		if substr is not None:
-			tokens += ['SUBSTR', substr.oid]
-		if syntax is not None:
-			tokens += ['SYNTAX', syntax.ref]
-		if single_value is not None:
-			tokens += ['SINGLE-VALUE']
-		if collective is not None:
-			tokens += ['COLLECTIVE']
-		if no_user_modification is not None:
-			tokens += ['NO-USER-MODIFICATION']
-		if usage is not None:
-			tokens += ['USAGE', usage.name]
-		tokens += [')']
-		self.schema_encoding = ' '.join(tokens)
-		self.oid = oid
-		self.name = name
-		self.names = [name] if name is not None else []
-		self.inherited_names = set(self.names)
-		self.obsolete = obsolete or False
-		self.sup = sup
-		if self.sup is not None:
-			self.inherited_names |= self.sup.inherited_names
-		self.equality = equality
-		if self.equality is None and self.sup is not None:
-			self.equality = self.sup.equality
-		self.ordering = ordering
-		if self.ordering is None and self.sup is not None:
-			self.ordering = self.sup.ordering
-		self.substr = substr
-		if self.substr is None and self.sup is not None:
-			self.substr = self.sup.substr
-		self.syntax = syntax
-		if self.syntax is None and self.sup is not None:
-			self.syntax = self.sup.syntax
-		self.single_value = single_value or False
-		self.collective = collective or False
-		self.no_user_modification = no_user_modification or False
-		self.usage = usage or AttributeTypeUsage.userApplications
-
-	def to_definition(self):
-		'''Get string reperesentation as used in attributeTypes attribute'''
-		return self.schema_encoding
-
-	def get_first_component_oid(self):
-		'''Used by objectIdentifierFirstComponentMatch'''
-		return self.oid
-
-	def __repr__(self):
-		return f'<ldapserver.schema.AttributeType {self.oid}>'
-
-class ObjectClassKind(enum.Enum):
-	'''Values for kind argument of `ObjectClass`'''
-	ABSTRACT = enum.auto()
-	STRUCTURAL = enum.auto()
-	AUXILIARY = enum.auto()
+		:type final_substring: bytes or None
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		if self.equality is None or self.substr is None:
+			raise exceptions.LDAPInappropriateMatching()
+		if inital_substring:
+			inital_substring = self.equality.syntax.decode(inital_substring)
+		any_substrings = [self.equality.syntax.decode(substring) for substring in any_substrings]
+		if final_substring:
+			final_substring = self.equality.syntax.decode(final_substring)
+		return self.substr.match_substr(attribute_values, inital_substring, any_substrings, final_substring)
+
+	def match_approx(self, attribute_values, assertion_value):
+		'''Return whether any attribute value approximatly equals assertion value
+
+		:param attribute_values: Attribute values (type according to syntax)
+		:type attribute_values: List of any
+		:param assertion_value: Assertion value
+		:type assertion_value: bytes
+		:returns: True if any attribute values matches, False otherwise
+		:rtype: bool
+		:raises exceptions.LDAPError: if the result is undefined'''
+		if self.equality is None:
+			raise exceptions.LDAPInappropriateMatching()
+		assertion_value = self.equality.syntax.decode(assertion_value)
+		return self.equality.match_approx(attribute_values, assertion_value)
+
+	def match_greater_or_equal(self, attribute_values, assertion_value):
+		if self.ordering is None:
+			raise exceptions.LDAPInappropriateMatching()
+		assertion_value = self.ordering.syntax.decode(assertion_value)
+		return self.ordering.match_greater_or_equal(attribute_values, assertion_value)
+
+	def __match_less(self, attribute_values, assertion_value):
+		if self.ordering is None:
+			raise exceptions.LDAPInappropriateMatching()
+		assertion_value = self.ordering.syntax.decode(assertion_value)
+		return self.ordering.match_less(attribute_values, assertion_value)
+
+	def match_less_or_equal(self, attribute_values, assertion_value):
+		equal_exc = None
+		try:
+			if self.match_equal(attribute_values, assertion_value):
+				return True
+		except exceptions.LDAPError as exc:
+			equal_exc = exc
+		if self.__match_less(attribute_values, assertion_value):
+			return True
+		if equal_exc is not None:
+			raise equal_exc
+		return False
+
+	def match_extensible(self, attribute_values, assertion_value, matching_rule=None):
+		if not matching_rule:
+			matching_rule = self.equality
+		if not matching_rule or matching_rule not in self.compatible_matching_rules:
+			raise exceptions.LDAPInappropriateMatching()
+		assertion_value = matching_rule.syntax.decode(assertion_value)
+		return matching_rule.match_extensible(attribute_values, assertion_value)
 
 class ObjectClass:
 	'''Representation of an object class wihin a schema'''
-	# pylint: disable=too-many-arguments
-	def __init__(self, oid, name=None, desc=None, obsolete=None, sup=None,
-	             kind=None, must=None, may=None):
-		tokens = ['(', oid]
-		if name is not None:
-			tokens += ['NAME', "'"+escape(name)+"'"] # name is actually a list
-		if desc is not None:
-			tokens += ['DESC', "'"+escape(desc)+"'"]
-		if obsolete is not None:
-			tokens += ['OBSOLETE', obsolete]
-		if sup is not None:
-			tokens += ['SUP', sup.name]
-		if kind is not None:
-			tokens += [kind.name]
-		if must and len(must) == 1:
-			tokens += ['MUST', must[0].name]
-		elif must and len(must) > 1:
-			tokens += ['MUST', '(']
-			for index, attr in enumerate(must):
-				if index > 0:
-					tokens += ['$']
-				tokens += [attr.name]
-			tokens += [')']
-		if may and len(may) == 1:
-			tokens += ['MAY', may[0].name]
-		elif may and len(may) > 1:
-			tokens += ['MAY', '(']
-			for index, attr in enumerate(may):
-				if index > 0:
-					tokens += ['$']
-				tokens += [attr.name]
-			tokens += [')']
-		tokens += [')']
-		self.schema_encoding = ' '.join(tokens)
-		self.oid = oid
-		self.name = name
-		self.names = [name]
-		self.desc = desc
-		self.obsolete = obsolete or False
-		self.sup = sup
-		self.kind = kind or ObjectClassKind.STRUCTURAL
-		self.must = must or []
-		self.may = may or []
-
-	def to_definition(self):
-		'''Get string reperesentation as used in objectClasses attribute'''
-		return self.schema_encoding
-
-	def get_first_component_oid(self):
-		'''Used by objectIdentifierFirstComponentMatch'''
-		return self.oid
+	def __init__(self, schema, definition):
+		self.schema = schema
+		self.definition = definition
+		self.oid = definition.oid
+		self.names = definition.name
+		self.ref = self.names[0] if self.names else self.oid
+		# Lookup dependencies to ensure consistency
+		# pylint: disable=pointless-statement
+		for sup_oid in definition.sup:
+			schema.object_classes[sup_oid]
+		for must_oid in definition.must:
+			schema.attribute_types[must_oid]
+		for may_oid in definition.may:
+			schema.attribute_types[may_oid]
+		schema._register(self, self.oid, self.ref, *self.names)
+		schema.object_classes._register(self, self.oid, self.ref, *self.names)
 
 	def __repr__(self):
-		return f'<ldapserver.schema.ObjectClass {self.schema_encoding}>'
-
-DOTTED_DECIMAL_RE = re.compile(r'[0-9]+(\.[0-9]+)*')
+		return f'<ldapserver.schema.ObjectClass {self.ref}>'
 
-def normalize_oid(oid):
-	return oid.lower().strip()
+class OIDDict(collections.abc.Mapping):
+	def __init__(self):
+		self.__data = {}
+		self.__refs = []
+		self.__numeric_oid_map = {}
 
-class Schema:
+	def _register(self, obj, oid, ref, *names):
+		if obj in self.__data:
+			return
+		if oid in self.__data:
+			raise Exception(f'OID "{oid}" already registered')
+		for name in (ref,) + names:
+			if name.lower().strip() in self.__data:
+				raise Exception(f'Short descriptive name "{name}" already registered')
+		self.__refs.append(ref)
+		self.__data[obj] = obj
+		self.__numeric_oid_map[obj] = oid
+		for name in (oid, ref) + names:
+			self.__data[name.lower().strip()] = obj
+			self.__numeric_oid_map[name.lower().strip()] = oid
+
+	def __getitem__(self, key):
+		if isinstance(key, str):
+			key = key.lower().strip()
+		return self.__data[key]
+
+	def __iter__(self):
+		return iter(self.__refs)
+
+	def __len__(self):
+		return len(self.__refs)
+
+	def get_numeric_oid(self, key, default=None):
+		if isinstance(key, str):
+			key = key.lower().strip()
+		return self.__numeric_oid_map.get(key, default)
+
+class Schema(OIDDict):
 	'''Collection of LDAP syntaxes, matching rules, attribute types and object
 	classes forming an LDAP schema.'''
-	def __init__(self, object_classes=None, attribute_types=None, matching_rules=None, syntaxes=None):
-		self.syntaxes = []
-		self.matching_rules = []
-		self.attribute_types = []
-		self.user_attribute_types = []
-		self.object_classes = []
-		self.__attribute_type_by_oid = {}
-		self.__attribute_type_and_subtypes_by_oid = {}
-		self.__oid_names = {}
-		for syntax in syntaxes:
-			self.register_syntax(syntax)
-		for matching_rule in matching_rules:
-			self.register_matching_rule(matching_rule)
-		for attribute_type in attribute_types:
-			self.register_attribute_type(attribute_type)
-		for object_class in object_classes:
-			self.register_object_class(object_class)
-
-	def extend(self, *schemas, object_classes=None, attribute_types=None, matching_rules=None, syntaxes=None):
-		'''Return new Schema instance with all object classes, attribute types,
-		matching rules and syntaxes from this schema and additional ones.
-
-		:return: Schema object with all schema data combined
-		:rtype: Schema'''
-		object_classes = self.object_classes + (object_classes or [])
-		attribute_types = self.attribute_types + (attribute_types or [])
-		matching_rules = self.matching_rules + (matching_rules or [])
-		syntaxes = self.syntaxes + (syntaxes or [])
-		for schema in schemas:
-			object_classes += schema.object_classes
-			attribute_types += schema.attribute_types
-			matching_rules += schema.matching_rules
-			syntaxes += schema.syntaxes
-		return Schema(object_classes, attribute_types, matching_rules, syntaxes)
-
-	def register_syntax(self, syntax):
-		'''Add syntax to schema
-
-		:param syntax: Syntax (subclass, not instance!) to add
-		:type syntax: Syntax subclass'''
-		if syntax in self.syntaxes:
-			return
-		self.register_oid(syntax.oid)
-		self.syntaxes.append(syntax)
-
-	def register_matching_rule(self, matching_rule):
-		'''Add matching rule and the referenced syntax to schema
-
-		:param matching_rule: Matching rule to add
-		:type matching_rule: MatchingRule'''
-		if matching_rule in self.matching_rules:
-			return
-		self.register_syntax(type(matching_rule.syntax))
-		self.register_oid(matching_rule.oid, *matching_rule.names)
-		self.matching_rules.append(matching_rule)
-
-	def register_attribute_type(self, attribute_type):
-		'''Add attribute type and all referenced matching rules and syntaxes to schema
-
-		:param attribute_type: Attribute type to add
-		:type attribute_type: AttributeType'''
-		if attribute_type in self.attribute_types:
-			return
-		self.register_syntax(type(attribute_type.syntax))
-		if attribute_type.equality:
-			self.register_matching_rule(attribute_type.equality)
-		if attribute_type.ordering:
-			self.register_matching_rule(attribute_type.ordering)
-		if attribute_type.substr:
-			self.register_matching_rule(attribute_type.substr)
-		self.register_oid(attribute_type.oid, *attribute_type.names)
-		for oid in [attribute_type.oid] + attribute_type.names:
-			self.__attribute_type_by_oid[normalize_oid(oid)] = attribute_type
-			self.__attribute_type_and_subtypes_by_oid.setdefault(normalize_oid(oid), [])
-			self.__attribute_type_and_subtypes_by_oid[normalize_oid(oid)].append(attribute_type)
-		for oid in attribute_type.inherited_names:
-			self.__attribute_type_and_subtypes_by_oid.setdefault(normalize_oid(oid), [])
-			self.__attribute_type_and_subtypes_by_oid[normalize_oid(oid)].append(attribute_type)
-		self.attribute_types.append(attribute_type)
-		if attribute_type.usage == AttributeTypeUsage.userApplications:
-			self.user_attribute_types.append(attribute_type)
-
-	def register_object_class(self, object_class):
-		'''Add object class and all referenced attribute types, matching rules and syntaxes to schema
-
-		:param object_class: Object class to add
-		:type object_class: ObjectClass'''
-		if object_class in self.object_classes:
-			return
-		for attribute_type in object_class.may + object_class.must:
-			self.register_attribute_type(attribute_type)
-		self.register_oid(object_class.oid, *object_class.names)
-		self.object_classes.append(object_class)
-
-	def get_attribute_type(self, oid):
-		'''Get attribute type by its OID
-
-		:param oid: Numeric OID or short descriptive name of attribute type
-		:type oid: str
-		:return: Attribute type identified by OID
-		:rtype: AttributeType
-		:raises KeyError: if attribute type is not found'''
-		attribute_type = self.__attribute_type_by_oid.get(normalize_oid(oid))
-		if attribute_type is None:
-			raise KeyError(f'Attribute "{oid}" not in schema')
-		return attribute_type
-
-	def get_attribute_type_and_subtypes(self, oid):
-		'''Get attribute type by its OID with all subtypes (if any)
-
-		:param oid: Numeric OID or short descriptive name of attribute type
-		:type oid: str
-		:return: List containing the attribute type and its subtypes or empty list
-		         if attribute type is not found
-		:rtype: List[AttributeType]'''
-		return self.__attribute_type_and_subtypes_by_oid.get(normalize_oid(oid), [])
-
-	def register_oid(self, numeric_oid, *names):
-		'''Register numeric OID and optionally short descriptive names with the schema
-
-		Both the numeric OID and all names must be unique within the schema.
-
-		:param numeric_oid: Numeric OID
-		:type numeric_oid: str
-		:param names: Short descriptive names for OID
-		:type names: str'''
-		for name in names:
-			if self.__oid_names.get(normalize_oid(name), numeric_oid) != numeric_oid:
-				raise Exception(f'OID short descriptive name "{name}" is already used in schema')
-		for name in names:
-			self.__oid_names[normalize_oid(name)] = numeric_oid
-
-	def get_numeric_oid(self, oid):
-		'''Return numeric OID for a given OID short descriptive name
-
-		If `oid` is a numeric OID it is returned normalized.
-
-		:param oid: OID short descriptive name or numeric OID
-		:type oid: str
-		:return: Numeric OID in dotted-decimal form or None if `oid` is not
-		         recognized and is not a numeric OID.
-		:rtype: str or None'''
-		oid = normalize_oid(oid)
-		if DOTTED_DECIMAL_RE.fullmatch(oid):
-			return oid
-		return self.__oid_names.get(oid)
+	# pylint: disable=too-many-branches,too-many-statements
+	def __init__(self, object_class_definitions=None, attribute_type_definitions=None,
+	             matching_rule_definitions=None, syntax_definitions=None):
+		super().__init__()
+		syntaxes_by_tag = {}
+
+		# Add syntaxes
+		self.syntaxes = OIDDict()
+		for definition in syntax_definitions or []:
+			if definition.oid not in self.syntaxes:
+				Syntax(self, definition, syntaxes_by_tag)
+		self.syntax_definitions = [syntax.definition for syntax in self.syntaxes.values()]
+
+		# Add matching rules
+		self.matching_rules = OIDDict()
+		for definition in matching_rule_definitions or []:
+			if definition.kind == MatchingRuleKind.EQUALITY:
+				cls = EqualityMatchingRule
+			elif definition.kind == MatchingRuleKind.ORDERING:
+				cls = OrderingMatchingRule
+			elif definition.kind == MatchingRuleKind.SUBSTR:
+				cls = SubstrMatchingRule
+			else:
+				raise ValueError('Invalid matching rule kind')
+			if definition.oid not in self.matching_rules:
+				cls(self, definition, syntaxes_by_tag)
+		self.matching_rule_definitions = [matching_rule.definition for matching_rule in self.matching_rules.values()]
+
+		# Add attribute types
+		attribute_type_definitions = [AttributeTypeDefinition.from_str(item)
+		                              if isinstance(item, str) else item
+		                              for item in attribute_type_definitions or []]
+		self.attribute_types = OIDDict()
+		self.user_attribute_types = set()
+		# Attribute types may refer to other (superior) attribute types. To resolve
+		# these dependencies we cycle through the definitions, each time adding
+		# those not added yet with fulfilled dependencies. Finally we add all the
+		# remaining ones to provoke exceptions.
+		keep_running = True
+		while keep_running:
+			keep_running = False
+			for definition in attribute_type_definitions:
+				if definition.oid in self.attribute_types:
+					continue
+				if definition.sup and definition.sup not in self.attribute_types:
+					continue
+				AttributeType(self, definition)
+				keep_running = True
+		for definition in attribute_type_definitions:
+			if definition.oid not in self.attribute_types:
+				AttributeType(self, definition)
+		self.attribute_type_definitions = [attribute_type.definition for attribute_type in self.attribute_types.values()]
+
+		# Add object classes
+		object_class_definitions = [ObjectClassDefinition.from_str(item)
+		                            if isinstance(item, str) else item
+		                            for item in object_class_definitions or []]
+		self.object_classes = OIDDict()
+		# Object classes may refer to other (superior) object classes. To resolve
+		# these dependencies we cycle through the definitions, each time adding
+		# those not added yet with fulfilled dependencies. Finally we add all the
+		# remaining ones to provoke exceptions.
+		keep_running = True
+		while keep_running:
+			keep_running = False
+			for definition in object_class_definitions:
+				if definition.oid in self.object_classes:
+					continue
+				if any(map(lambda oid: oid and oid not in self.object_classes, definition.sup)):
+					continue
+				ObjectClass(self, definition)
+				keep_running = True
+		for definition in object_class_definitions:
+			if definition.oid not in self.object_classes:
+				ObjectClass(self, definition)
+		self.object_class_definitions = [object_class.definition for object_class in self.object_classes.values()]
+
+		# Generate and add matching rules
+		self.matching_rule_use_definitions = []
+		for matching_rule in self.matching_rules.values():
+			definition = matching_rule.definition
+			applies = [attribute_type.ref for attribute_type in matching_rule.compatible_attribute_types]
+			rule_use = MatchingRuleUseDefinition(definition.oid, name=definition.name,
+					desc=definition.desc, obsolete=definition.obsolete, applies=applies,
+					extensions=definition.extensions)
+			if applies:
+				self.matching_rule_use_definitions.append(rule_use)
+
+	def extend(self, object_class_definitions=None, attribute_type_definitions=None,
+	           matching_rule_definitions=None, syntax_definitions=None):
+		syntax_definitions = self.syntax_definitions + (syntax_definitions or [])
+		matching_rule_definitions = self.matching_rule_definitions + (matching_rule_definitions or [])
+		attribute_type_definitions = self.attribute_type_definitions + (attribute_type_definitions or [])
+		object_class_definitions = self.object_class_definitions + (object_class_definitions or [])
+		return type(self)(syntax_definitions=syntax_definitions,
+		                  matching_rule_definitions=matching_rule_definitions,
+		                  attribute_type_definitions=attribute_type_definitions,
+		                  object_class_definitions=object_class_definitions)
+
+	def __or__(self, value):
+		return self.extend(syntax_definitions=value.syntax_definitions,
+		                   matching_rule_definitions=value.matching_rule_definitions,
+		                   attribute_type_definitions=value.attribute_type_definitions,
+		                   object_class_definitions=value.object_class_definitions)
diff --git a/ldapserver/server.py b/ldapserver/server.py
index 4c1235bc2455d5cbee0fc25a52bf95a57c31c021..d653f716cdd6deb6068e5750018c30e67b34e87a 100644
--- a/ldapserver/server.py
+++ b/ldapserver/server.py
@@ -9,7 +9,6 @@ import string
 import itertools
 
 from . import asn1, exceptions, ldap, schema, objects
-from .dn import DN
 
 def pop_control(controls, oid):
 	result = None
@@ -173,7 +172,7 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		mechansims. Attributes can be accessed in a dict-like fashion.
 	'''
 
-	subschema = objects.SubschemaSubentry(schema.RFC4519_SUBSCHEMA, 'cn=Subschema')
+	subschema = objects.SubschemaSubentry(schema.RFC4519_SCHEMA, 'cn=Subschema', cn=['Subschema'])
 	'''
 	.. py:attribute:: subschema
 
@@ -190,60 +189,25 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		super().setup()
 		self.rootdse = self.subschema.RootDSE()
 		self.rootdse['objectClass'] = ['top']
-		self.rootdse['supportedSASLMechanisms'] = self.get_sasl_mechanisms
-		self.rootdse['supportedExtension'] = self.get_extentions
-		self.rootdse['supportedControl'] = self.get_controls
-		self.rootdse['supportedLDAPVersion'] = ['3']
-		self.bind_object = None
-		self.bind_sasl_state = None
-		self.__paged_searches = {} # pagination cookie -> (iterator, orig_op)
-		self.__paged_cookie_counter = 0
-
-	def get_extentions(self):
-		'''Get supported LDAP extentions
-
-		:returns: OIDs of supported LDAP extentions
-		:rtype: list of strings
-
-		Called whenever the root DSE attribute "supportedExtension" is queried.'''
-		res = []
 		if self.supports_starttls:
-			res.append(ldap.STARTTLS_OID)
+			self.rootdse['supportedExtension'].append(ldap.STARTTLS_OID)
 		if self.supports_whoami:
-			res.append(ldap.WHOAMI_OID)
+			self.rootdse['supportedExtension'].append(ldap.WHOAMI_OID)
 		if self.supports_password_modify:
-			res.append(ldap.PASSWORD_MODIFY_OID)
-		return res
-
-	def get_controls(self):
-		'''Get supported LDAP controls
-
-		:returns: OIDs of supported LDAP controls
-		:rtype: list of strings
-
-		Called whenever the root DSE attribute "supportedControl" is queried.'''
-		res = []
+			self.rootdse['supportedExtension'].append(ldap.PASSWORD_MODIFY_OID)
 		if self.supports_paged_results:
-			res.append(ldap.PAGED_RESULTS_OID)
-		return res
-
-	def get_sasl_mechanisms(self):
-		'''Get supported SASL mechanisms
-
-		:returns: Names of supported SASL mechanisms
-		:rtype: list of strings
-
-		SASL mechanism name are typically all-caps, like "EXTERNAL".
-
-		Called whenever the root DSE attribute "supportedSASLMechanisms" is queried.'''
-		res = []
+			self.rootdse['supportedControl'].append(ldap.PAGED_RESULTS_OID)
 		if self.supports_sasl_anonymous:
-			res.append('ANONYMOUS')
+			self.rootdse['supportedSASLMechanisms'].append('ANONYMOUS')
 		if self.supports_sasl_plain:
-			res.append('PLAIN')
+			self.rootdse['supportedSASLMechanisms'].append('PLAIN')
 		if self.supports_sasl_external:
-			res.append('EXTERNAL')
-		return res
+			self.rootdse['supportedSASLMechanisms'].append('EXTERNAL')
+		self.rootdse['supportedLDAPVersion'] = ['3']
+		self.bind_object = None
+		self.bind_sasl_state = None
+		self.__paged_searches = {} # pagination cookie -> (iterator, orig_op)
+		self.__paged_cookie_counter = 0
 
 	def handle_bind(self, op, controls=None):
 		reject_critical_controls(controls)
@@ -468,7 +432,8 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		paged_control = ldap.PagedResultsValue.from_ber(paged_control.controlValue)[0]
 		if not paged_control.cookie: # New paged search request
 			results = self.do_search(op.baseObject, op.scope, op.filter)
-			results = filter(lambda obj: obj.match_search(op.baseObject, op.scope, op.filter), results)
+			results = map(lambda obj: obj.search(op.baseObject, op.scope, op.filter, op.attributes, op.typesOnly), results)
+			results = filter(None, results)
 			iterator = iter(mark_last(results))
 		else: # Continue existing paged search
 			try:
@@ -483,10 +448,10 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		is_last = True
 		entries = 0
 		time_start = time.perf_counter()
-		for obj, is_last in itertools.islice(iterator, 0, paged_control.size):
-			self.logger.debug('SEARCH entry %r', obj)
-			yield obj.get_search_result_entry(op.attributes, op.typesOnly)
+		for entry, is_last in itertools.islice(iterator, 0, paged_control.size):
+			self.logger.debug('SEARCH entry %r', entry)
 			entries += 1
+			yield entry
 		cookie = b''
 		if not is_last:
 			cookie = str(self.__paged_cookie_counter).encode()
@@ -510,8 +475,8 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		entries = 0
 		time_start = time.perf_counter()
 		for obj in self.do_search(op.baseObject, op.scope, op.filter):
-			if obj.match_search(op.baseObject, op.scope, op.filter):
-				entry = obj.get_search_result_entry(op.attributes, op.typesOnly)
+			entry = obj.search(op.baseObject, op.scope, op.filter, op.attributes, op.typesOnly)
+			if entry:
 				self.logger.debug('SEARCH entry %r', entry)
 				entries += 1
 				yield entry
@@ -569,11 +534,15 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 		The default implementation calls `do_search` and returns the first object
 		with the right DN.'''
 		objs = self.do_search(dn, ldap.SearchScope.baseObject, ldap.FilterPresent(attribute='objectClass'))
-		dn = DN.from_str(dn)
 		for obj in objs:
-			if obj.dn == dn:
+			try:
+				obj.compare(dn)
 				return obj
-		return None
+			except exceptions.LDAPNoSuchObject:
+				pass
+			except exceptions.LDAPError:
+				return obj
+		raise exceptions.LDAPNoSuchObject()
 
 	def handle_unbind(self, op, controls=None):
 		self.logger.info('UNBIND')
@@ -592,6 +561,8 @@ class LDAPRequestHandler(BaseLDAPRequestHandler):
 			except Exception: # pylint: disable=broad-except
 				traceback.print_exc()
 				self.keep_running = False
+			if ldap.STARTTLS_OID in self.rootdse['supportedExtension']:
+				self.rootdse['supportedExtension'].remove(ldap.STARTTLS_OID)
 		elif op.requestName == ldap.WHOAMI_OID and self.supports_whoami:
 			self.logger.info('EXTENDED WHOAMI')
 			# "Who am I?" Operation (RFC 4532)
diff --git a/tests/test_objects.py b/tests/test_objects.py
new file mode 100644
index 0000000000000000000000000000000000000000..dae7801c0c7ebdd0cb13449e16e45f0200e2af3d
--- /dev/null
+++ b/tests/test_objects.py
@@ -0,0 +1,772 @@
+import unittest
+import datetime
+
+import ldapserver
+from ldapserver.objects import AttributeDict, Object, RootDSE, SubschemaSubentry, ObjectTemplate, WILDCARD_VALUE
+from ldapserver.dn import DN
+from ldapserver import ldap
+
+schema = ldapserver.schema.RFC4519_SCHEMA
+
+class TestAttributeDict(unittest.TestCase):
+	def test_init(self):
+		AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+
+	def test_getitem(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		self.assertEqual(attrs['cn'], ['foo', 'bar'])
+		self.assertEqual(attrs['CN'], ['foo', 'bar'])
+		self.assertEqual(attrs['2.5.4.3'], ['foo', 'bar'])
+		self.assertEqual(attrs[schema['cn']], ['foo', 'bar'])
+		self.assertEqual(attrs['uid'], [])
+		self.assertEqual(attrs['name'], [])
+		self.assertEqual(attrs['objectClass'], [])
+		attrs['objectClass'].append('top')
+		self.assertEqual(attrs['objectClass'], ['top'])
+		with self.assertRaises(KeyError):
+			attrs['foobar']
+
+	def test_get(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		self.assertEqual(attrs.get('cn'), ['foo', 'bar'])
+		self.assertEqual(attrs.get('CN'), ['foo', 'bar'])
+		self.assertEqual(attrs.get('2.5.4.3'), ['foo', 'bar'])
+		self.assertEqual(attrs.get(schema['cn']), ['foo', 'bar'])
+		self.assertEqual(attrs.get('uid'), [])
+		self.assertEqual(attrs.get('uid', ['default']), ['default'])
+		self.assertEqual(attrs.get('name'), [])
+		self.assertEqual(attrs.get('name', ['default']), ['default'])
+		self.assertEqual(attrs.get('name', subtypes=True), ['foo', 'bar'])
+		self.assertEqual(attrs.get('foobar'), [])
+
+	def test_contains(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		self.assertIn('cn', attrs)
+		self.assertNotIn('uid', attrs)
+		self.assertNotIn('objectClass', attrs)
+		attrs['objectClass'].append('top')
+		self.assertIn('objectClass', attrs)
+		self.assertNotIn('foobar', attrs)
+
+	def test_setitem(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		attrs['cn'] = ['bar', 'foo']
+		self.assertEqual(attrs['cn'], ['bar', 'foo'])
+		attrs['cn'] = []
+		self.assertEqual(attrs['cn'], [])
+		attrs['objectClass'] = []
+		self.assertEqual(attrs['objectClass'], [])
+		with self.assertRaises(KeyError):
+			attrs['foobar'] = ['test']
+
+	def test_delitem(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		del attrs['cn']
+		self.assertEqual(attrs['cn'], [])
+		del attrs['cn'] # does nothing
+		with self.assertRaises(KeyError):
+			del attrs['foobar']
+
+	def test_iter(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		self.assertEqual(list(attrs), ['cn'])
+
+	def test_len(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[])
+		self.assertEqual(len(attrs), 1)
+		attrs['objectClass'] = ['top']
+		self.assertEqual(len(attrs), 2)
+
+	def test_keys(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		self.assertEqual(set(attrs.keys()), {'cn', 'objectClass'})
+		self.assertEqual(set(attrs.keys(types=True)), {schema['cn'], schema['objectClass']})
+
+	def test_items(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		self.assertEqual(list(sorted(attrs.items())), [('cn', ['foo', 'bar']), ('objectClass', ['top'])])
+		self.assertIn((schema['cn'], ['foo', 'bar']), attrs.items(types=True))
+		self.assertIn((schema['objectClass'], ['top']), attrs.items(types=True))
+
+	def test_setdefault(self):
+		attrs = AttributeDict(schema, cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		self.assertEqual(attrs.setdefault('CN', ['default']), ['foo', 'bar'])
+		self.assertEqual(attrs['Cn'], ['foo', 'bar'])
+		self.assertEqual(attrs.setdefault('c', ['default']), ['default'])
+		self.assertEqual(attrs['c'], ['default'])
+
+class TestObject(unittest.TestCase):
+	def test_init(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[])
+		self.assertEqual(obj.dn, DN.from_str('cn=foo,dc=example,dc=com'))
+		self.assertEqual(obj['cn'], ['foo', 'bar'])
+		self.assertEqual(obj['uid'], [])
+
+	def test_match_search_dn(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', objectclass=['top'])
+		true_filter = ldap.FilterPresent('objectClass')
+
+		scope = ldap.SearchScope.baseObject
+		self.assertTrue(obj.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=bar,dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+		scope = ldap.SearchScope.singleLevel
+		self.assertFalse(obj.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=bar,dc=example,dc=com', scope, true_filter))
+		self.assertTrue(obj.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+		scope = ldap.SearchScope.wholeSubtree
+		self.assertTrue(obj.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=bar,dc=example,dc=com', scope, true_filter))
+		self.assertTrue(obj.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertTrue(obj.match_search('', scope, true_filter))
+		self.assertFalse(obj.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+	def test_match_search_filter_present(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		# True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterPresent('ObjectClass')))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterPresent('2.5.4.3'))) # OID
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterPresent('name'))) # subtype
+		# False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterPresent('uid')))
+		# Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterPresent('undefined')))
+
+	def test_match_search_filter_not(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('ObjectClass'))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('2.5.4.3')))) # OID of cn
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('name')))) # subtype
+		# Not False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('uid'))))
+		# Not Undefined = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('undefined'))))
+
+	def test_match_search_filter_and(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		true = ldap.FilterPresent('objectclass')
+		false = ldap.FilterPresent('uid')
+		undefined = ldap.FilterPresent('undefined')
+		# True = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterAnd([true])))
+		# True and True = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterAnd([true, true])))
+		# True and False = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([true, false])))
+		# False and False = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([false, false])))
+		# False and Undefined = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([false, undefined])))
+		# True and Undefined = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterAnd([true, undefined])))
+
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true]))))
+		# Not (True and True) = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, true]))))
+		# Not (True and False) = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, false]))))
+		# Not (False and False) = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, false]))))
+		# Not (False and Undefined) = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, undefined]))))
+		# Not (True and Undefined) = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, undefined]))))
+
+	def test_match_search_filter_or(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		true = ldap.FilterPresent('objectclass')
+		false = ldap.FilterPresent('uid')
+		undefined = ldap.FilterPresent('undefined')
+		# True = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterOr([true])))
+		# True or True = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterOr([true, true])))
+		# True or False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterOr([true, false])))
+		# False or False = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterOr([false, false])))
+		# True or Undefined = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterOr([true, undefined])))
+		# False or Undefined = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterOr([false, undefined])))
+
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true]))))
+		# Not (True or True) = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, true]))))
+		# Not (True or False) = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, false]))))
+		# Not (False or False) = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, false]))))
+		# Not (True or Undefined) = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, undefined]))))
+		# Not (False or Undefined) = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, undefined]))))
+
+	def test_match_search_filter_equal(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		# True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterEqual('ObjectClass', b'top')))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterEqual('2.5.4.3', b'Foo'))) # OID
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterEqual('name', b'bar'))) # subtype
+		# False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterEqual('ObjectClass', b'Person')))
+		# Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterEqual('undefined', b'foo')))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterEqual('telexNumber', b'foo'))) # no EQUALITY
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('ObjectClass', b'top'))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('2.5.4.3', b'Foo')))) # OID
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('name', b'bar')))) # subtype
+		# Not False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('ObjectClass', b'Person'))))
+		# Not Undefined = Undefined (behaves like False)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('undefined', b'foo'))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('telexNumber', b'foo')))) # no EQUALITY
+
+	def test_match_search_filter_substr(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foobar', 'test'], uid=[], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'foo')])))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'bar')])))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'foo'), ldap.AnySubstring(b'b'), ldap.AnySubstring(b'a'), ldap.FinalSubstring(b'r')])))
+
+	def test_match_search_filter_le(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo'], objectclass=['top'], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161032Z')))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161033Z')))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161031Z')))
+		# LessOrEqual is hybrid between EQUALITY and ORDERING
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterLessOrEqual('cn', b'foo')))
+
+	def test_match_search_filter_ge(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo'], objectclass=['top'], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161032Z')))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161033Z')))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161031Z')))
+		# GreaterOrEqual is only ORDERING (which cn does not have)
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterGreaterOrEqual('cn', b'foo')))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('cn', b'foo'))))
+
+	def test_match_search_filter_extensible_attribute_type(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'test'], uid=['foobar'], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		# True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch(None, 'uid', b'Foobar', False)))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'Foobar', False)))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreSubstringsMatch', 'uid', b'F*b*r', False)))
+		# False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseExactMatch', 'uid', b'Foobar', False)))
+		# Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseExactMatch', 'createTimestamp', b'199412161032Z', False)))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('generalizedTimeMatch', 'cn', b'199412161032Z', False)))
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch(None, 'uid', b'Foobar', False))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'Foobar', False))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreSubstringsMatch', 'uid', b'F*b*r', False))))
+		# Not False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseExactMatch', 'uid', b'Foobar', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseExactMatch', 'createTimestamp', b'199412161032Z', False))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('generalizedTimeMatch', 'cn', b'199412161032Z', False))))
+
+	def test_match_search_filter_extensible_no_attribute_type(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'test'], uid=['foobar'], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		# True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foobar', False)))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'top', False)))
+		# False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'person', False)))
+		# Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('octetStringOrderingMatch', None, b'someoctetstring', False)))
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foobar', False))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'top', False))))
+		# Not False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'person', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('octetStringOrderingMatch', None, b'someoctetstring', False))))
+
+	def test_match_search_filter_extensible_dn(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'test'], uid=['foobar'], objectclass=['top'])
+		dn = 'cn=foo,dc=example,dc=com'
+		scope = ldap.SearchScope.baseObject
+		obj = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', True)))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'foobar', True))) # also matches regular attributes
+		# False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', False)))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'somethingelse', False)))
+		# Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterExtensibleMatch('generalizedTimeMatch', 'dc', b'example', False)))
+		# Not True = False
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', True))))
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'foobar', True)))) # also matches regular attributes
+		# Not False = True
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', False))))
+		self.assertTrue(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'somethingelse', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(obj.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('generalizedTimeMatch', 'dc', b'example', False))))
+
+	def test_search(self):
+		class TrueObject(Object):
+			def match_search(self, base_obj, scope, filter_obj):
+				return True
+
+		class FalseObject(Object):
+			def match_search(self, base_obj, scope, filter_obj):
+				return False
+
+		obj = FalseObject(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'], subschemaSubentry=[DN('cn=subschema')])
+		self.assertIsNone(obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), [], False))
+		obj = TrueObject(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'], subschemaSubentry=[DN('cn=subschema')])
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), [], False)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 2)
+		self.assertEqual({item.type: item.vals for item in result.attributes},
+		                 {'cn': [b'foo', b'bar'], 'objectClass': [b'top']})
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['*'], False)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 2)
+		self.assertEqual({item.type: item.vals for item in result.attributes},
+		                 {'cn': [b'foo', b'bar'], 'objectClass': [b'top']})
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['1.1'], False)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 0)
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['cn', 'subschemaSubentry', 'foobar'], False)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 2)
+		self.assertEqual({item.type: item.vals for item in result.attributes},
+		                 {'cn': [b'foo', b'bar'], 'subschemaSubentry': [b'cn=subschema']})
+		result = obj.search('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass'), ['cn', 'uid', 'subschemaSubentry', 'foobar'], True)
+		self.assertEqual(result.objectName, 'cn=foo,dc=example,dc=com')
+		self.assertEqual(len(result.attributes), 2)
+		self.assertEqual({item.type: item.vals for item in result.attributes},
+		                 {'cn': [], 'subschemaSubentry': []})
+
+	def test_compare(self):
+		obj = Object(schema, 'cn=foo,dc=example,dc=com', cn=['foo', 'bar'], uid=[], objectclass=['top'])
+		self.assertTrue(obj.compare('cn=foo,dc=example,dc=com', 'cn', b'bar'))
+		self.assertFalse(obj.compare('cn=foo,dc=example,dc=com', 'cn', b'test'))
+		with self.assertRaises(ldapserver.exceptions.LDAPUndefinedAttributeType):
+			obj.compare('cn=foo,dc=example,dc=com', 'foobar', b'test')
+		with self.assertRaises(ldapserver.exceptions.LDAPNoSuchObject):
+			obj.compare('cn=bar,dc=example,dc=com', 'cn', b'test')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			obj.compare('cn=foo,dc=example,dc=com', 'objectclass', b'undefined')
+
+class TestRootDSE(unittest.TestCase):
+	def test_init(self):
+		obj = RootDSE(schema)
+		self.assertEqual(obj.dn, DN())
+		obj = RootDSE(schema, cn=['foo', 'bar'])
+		self.assertEqual(obj.dn, DN())
+		self.assertEqual(obj['cn'], ['foo', 'bar'])
+
+	def test_match_search(self):
+		obj = RootDSE(schema, cn=['foo', 'bar'], objectclass=['top'])
+		self.assertTrue(obj.match_search('', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass')))
+		self.assertFalse(obj.match_search('cn=root', ldap.SearchScope.baseObject, ldap.FilterPresent('objectclass')))
+		self.assertFalse(obj.match_search('', ldap.SearchScope.singleLevel, ldap.FilterPresent('objectclass')))
+		self.assertFalse(obj.match_search('', ldap.SearchScope.wholeSubtree, ldap.FilterPresent('objectclass')))
+		self.assertFalse(obj.match_search('', ldap.SearchScope.baseObject, ldap.FilterPresent('cn')))
+
+class TestObjectTemplate(unittest.TestCase):
+	def test_init(self):
+		obj = ObjectTemplate(schema, 'ou=users,dc=example,dc=com', 'uid', cn=['foo', 'bar'], uid=[])
+
+	def test_match_search_dn(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		true_filter = ldap.FilterPresent('objectClass')
+
+		scope = ldap.SearchScope.baseObject
+		self.assertTrue(template.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertFalse(template.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertFalse(template.match_search('', scope, true_filter))
+		self.assertFalse(template.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+		scope = ldap.SearchScope.singleLevel
+		self.assertFalse(template.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertTrue(template.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertFalse(template.match_search('', scope, true_filter))
+		self.assertFalse(template.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+		scope = ldap.SearchScope.wholeSubtree
+		self.assertTrue(template.match_search('cn=foo,dc=example,dc=com', scope, true_filter))
+		self.assertTrue(template.match_search('dc=example,dc=com', scope, true_filter))
+		self.assertTrue(template.match_search('', scope, true_filter))
+		self.assertFalse(template.match_search('cn=test,cn=foo,dc=example,dc=com', scope, true_filter))
+
+	def test_match_search_filter_present(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterPresent('objectclass')))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterPresent('uid')))
+		# Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterPresent('undefined')))
+		# Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterPresent('cn')))
+		# We verify in ..._filter_not that Undefined/Maybe are not just False/True
+
+	def test_match_search_filter_not(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('objectclass'))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('uid'))))
+		# Not Undefined = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('undefined'))))
+		# Not Maybe = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterPresent('cn'))))
+
+	def test_match_search_filter_and(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		true = ldap.FilterPresent('objectclass')
+		false = ldap.FilterPresent('uid')
+		undefined = ldap.FilterPresent('undefined')
+		maybe = ldap.FilterPresent('cn')
+
+		# True = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterAnd([true])))
+		# True and True = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterAnd([true, true])))
+		# True and False = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([true, false])))
+		# False and False = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([false, false])))
+		# False and Undefined = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([false, undefined])))
+		# True and Undefined = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([true, undefined])))
+		# False and Maybe = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([false, maybe])))
+		# True and Maybe = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterAnd([true, maybe])))
+		# Undefined and Maybe = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterAnd([undefined, maybe])))
+
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true]))))
+		# Not (True and True) = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, true]))))
+		# Not (True and False) = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, false]))))
+		# Not (False and False) = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, false]))))
+		# Not (False and Undefined) = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, undefined]))))
+		# Not (True and Undefined) = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, undefined]))))
+		# Not (False and Maybe) = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([false, maybe]))))
+		# Not (True and Maybe) = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([true, maybe]))))
+		# Not (Undefined and Maybe) = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterAnd([undefined, maybe]))))
+
+	def test_match_search_filter_or(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		true = ldap.FilterPresent('objectclass')
+		false = ldap.FilterPresent('uid')
+		undefined = ldap.FilterPresent('undefined')
+		maybe = ldap.FilterPresent('cn')
+
+		# True = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([true])))
+		# True or True = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([true, true])))
+		# True or False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([true, false])))
+		# False or False = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterOr([false, false])))
+		# True or Undefined = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([true, undefined])))
+		# False or Undefined = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterOr([false, undefined])))
+		# True or Maybe = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([true, maybe])))
+		# False or Maybe = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([false, maybe])))
+		# Undefined or Maybe = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterOr([undefined, maybe])))
+
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true]))))
+		# Not (True or True) = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, true]))))
+		# Not (True or False) = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, false]))))
+		# Not (False or False) = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, false]))))
+		# Not (True or Undefined) = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, undefined]))))
+		# Not (False or Undefined) = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, undefined]))))
+		# Not (True or Maybe) = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([true, maybe]))))
+		# Not (False or Maybe) = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([false, maybe]))))
+		# Not (Undefined or Maybe) = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterOr([undefined, maybe]))))
+
+	def test_match_search_filter_equal(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterEqual('ObjectClass', b'top')))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterEqual('ObjectClass', b'Person')))
+		# Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterEqual('undefined', b'foo')))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterEqual('telexNumber', b'foo'))) # no EQUALITY
+		# Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterEqual('cn', b'foo')))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterEqual('2.5.4.3', b'Foo'))) # OID
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterEqual('name', b'bar'))) # subtype
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('ObjectClass', b'top'))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('ObjectClass', b'Person'))))
+		# Not Undefined = Undefined (behaves like False)
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('undefined', b'foo'))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('telexNumber', b'foo')))) # no EQUALITY
+		# Not Maybe = Maybe (behaves like True)
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('cn', b'Foo'))))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('2.5.4.3', b'Foo')))) # OID
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterEqual('name', b'bar')))) # subtype
+
+	def test_match_search_filter_substr(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar', 'test'])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterSubstrings('uid', [ldap.InitialSubstring(b'foo')])))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterSubstrings('uid', [ldap.InitialSubstring(b'bar')])))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterSubstrings('objectclass', [ldap.InitialSubstring(b'foo')])))
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'foo')])))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterSubstrings('uid', [ldap.InitialSubstring(b'foo')]))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterSubstrings('uid', [ldap.InitialSubstring(b'bar')]))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterSubstrings('objectclass', [ldap.InitialSubstring(b'foo')]))))
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterSubstrings('cn', [ldap.InitialSubstring(b'foo')]))))
+
+	def test_match_search_filter_le(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161032Z')))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161033Z')))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterLessOrEqual('objectclass', b'top'))) # LessOrEqual is hybrid between EQUALITY and ORDERING
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'199412161031Z')))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterLessOrEqual('createTimestamp', b'invalid-date')))
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterLessOrEqual('modifyTimestamp', b'199412161032Z')))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('createTimestamp', b'199412161032Z'))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('createTimestamp', b'199412161033Z'))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('objectclass', b'top')))) # LessOrEqual is hybrid between EQUALITY and ORDERING
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('createTimestamp', b'199412161031Z'))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('createTimestamp', b'invalid-date'))))
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterLessOrEqual('modifyTimestamp', b'199412161032Z'))))
+
+	def test_match_search_filter_ge(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], createTimestamp=[datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)], modifyTimestamp=[WILDCARD_VALUE])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161032Z')))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161031Z')))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'199412161033Z')))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('createTimestamp', b'invalid-date')))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('objectclass', b'top'))) # GreaterOrEqual is only ORDERING
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterGreaterOrEqual('modifyTimestamp', b'199412161032Z')))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('createTimestamp', b'199412161032Z'))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('createTimestamp', b'199412161031Z'))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('createTimestamp', b'199412161033Z'))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('createTimestamp', b'invalid-date'))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('objectclass', b'top')))) # GreaterOrEqual is only ORDERING
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterGreaterOrEqual('modifyTimestamp', b'199412161032Z'))))
+
+	def test_match_search_filter_extensible_attribute_type(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch(None, 'uid', b'Foobar', False)))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'Foobar', False)))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreSubstringsMatch', 'uid', b'F*b*r', False)))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseExactMatch', 'uid', b'Foobar', False)))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseExactMatch', 'createTimestamp', b'199412161032Z', False)))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('generalizedTimeMatch', 'cn', b'199412161032Z', False)))
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'Foobar', False)))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch(None, 'uid', b'Foobar', False))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'Foobar', False))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreSubstringsMatch', 'uid', b'F*b*r', False))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseExactMatch', 'uid', b'Foobar', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseExactMatch', 'createTimestamp', b'199412161032Z', False))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('generalizedTimeMatch', 'cn', b'199412161032Z', False))))
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'Foobar', False))))
+
+	def test_match_search_filter_extensible_no_attribute_type(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foobar', False)))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'top', False)))
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'person', False)))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('octetStringOrderingMatch', None, b'someoctetstring', False)))
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foo', False)))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foobar', False))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'top', False))))
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('objectIdentifierMatch', None, b'person', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('octetStringOrderingMatch', None, b'someoctetstring', False))))
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', None, b'foo', False))))
+
+	def test_match_search_filter_extensible_dn(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		dn = 'dc=example,dc=com'
+		scope = ldap.SearchScope.wholeSubtree
+		# True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', True)))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'foobar', True))) # also matches regular attributes
+		# False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', False)))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'somethingelse', False)))
+		# Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterExtensibleMatch('generalizedTimeMatch', 'dc', b'example', False)))
+		# Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'foo', True)))
+		# Not True = False
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', True))))
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'uid', b'foobar', True)))) # also matches regular attributes
+		# Not False = True
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'example', False))))
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'dc', b'somethingelse', False))))
+		# Not Undefined = Undefined
+		self.assertFalse(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('generalizedTimeMatch', 'dc', b'example', False))))
+		# Not Maybe = Maybe
+		self.assertTrue(template.match_search(dn, scope, ldap.FilterNot(ldap.FilterExtensibleMatch('caseIgnoreMatch', 'cn', b'foo', True))))
+
+	def test_extract_search_constraints(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], uid=['foobar'])
+		self.assertEqual(dict(template.extract_search_constraints('dc=exapmle,dc=com', ldap.SearchScope.wholeSubtree, ldap.FilterEqual('cn', b'foo')).items()), {'cn': ['foo']})
+		self.assertEqual(dict(template.extract_search_constraints('dc=exapmle,dc=com', ldap.SearchScope.wholeSubtree, ldap.FilterAnd([ldap.FilterEqual('objectclass', b'top'), ldap.FilterEqual('cn', b'foo')])).items()), {'cn': ['foo'], 'objectClass': ['top']})
+		self.assertEqual(dict(template.extract_search_constraints('cn=foo,dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectClass')).items()), {'cn': ['foo']})
+		self.assertEqual(dict(template.extract_search_constraints('dc=example,dc=com', ldap.SearchScope.baseObject, ldap.FilterPresent('objectClass')).items()), {})
+
+	def test_create_object(self):
+		template = ObjectTemplate(schema, 'dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE], c=[WILDCARD_VALUE], uid=['foobar'])
+		obj = template.create_object('foo', cn=['foo', 'bar'], c=['DE'])
+		self.assertEqual(obj.dn, DN('cn=foo,dc=example,dc=com'))
+		self.assertEqual(dict(obj.items()), {'cn': ['foo', 'bar'], 'uid': ['foobar'], 'c': ['DE'], 'objectClass': ['top']})
+		obj = template.create_object('foo', cn=['foo', 'bar'])
+		self.assertEqual(dict(obj.items()), {'cn': ['foo', 'bar'], 'uid': ['foobar'], 'objectClass': ['top']})
+		with self.assertRaises(ValueError):
+			template.create_object('foo', cn=['foo', 'bar'], c=['DE'], description=['foo bar'])
+
+class TestSubschemaSubentry(unittest.TestCase):
+	def test_init(self):
+		obj = SubschemaSubentry(schema, 'cn=Subschema', cn=['Subschema'])
+		self.assertIn('subschema', obj['objectClass'])
+
+	def test_match_search(self):
+		obj = SubschemaSubentry(schema, 'cn=Subschema', cn=['Subschema'])
+		self.assertIn("( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )", [str(item) for item in obj['objectClasses']])
+		self.assertIn("( 1.3.6.1.4.1.1466.115.121.1.15 DESC 'Directory String' )", [str(item) for item in obj['ldapSyntaxes']])
+		self.assertIn("( 2.5.13.5 NAME 'caseExactMatch' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )", [str(item) for item in obj['matchingRules']])
+		self.assertIn("( 2.5.21.6 NAME 'objectClasses' EQUALITY objectIdentifierFirstComponentMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.37 USAGE directoryOperation )", [str(item) for item in obj['attributeTypes']])
+		[str(item) for item in obj['matchingRuleUse']]
+
+	def test_constructors(self):
+		subschema = SubschemaSubentry(schema, 'cn=Subschema', cn=['Subschema'])
+		attrs = subschema.AttributeDict(cn=['foo'])
+		self.assertIsInstance(attrs, AttributeDict)
+		self.assertIs(attrs.schema, subschema.schema)
+		self.assertEqual(attrs['cn'], ['foo'])
+		obj = subschema.Object('cn=foo,dc=example,dc=com', cn=['foo'])
+		self.assertIsInstance(obj, Object)
+		self.assertIs(obj.schema, subschema.schema)
+		self.assertEqual(obj.dn, DN('cn=foo,dc=example,dc=com'))
+		self.assertEqual(obj['cn'], ['foo'])
+		self.assertEqual(obj['subschemaSubentry'], [DN('cn=Subschema')])
+		rootdse = subschema.RootDSE(cn=['foo'])
+		self.assertIsInstance(rootdse, RootDSE)
+		self.assertIs(rootdse.schema, subschema.schema)
+		self.assertEqual(rootdse['cn'], ['foo'])
+		template = subschema.ObjectTemplate('dc=example,dc=com', 'cn', objectclass=['top'], cn=[WILDCARD_VALUE])
+		self.assertIsInstance(template, ObjectTemplate)
+		self.assertIs(template.schema, subschema.schema)
+		self.assertEqual(template['subschemaSubentry'], [DN('cn=Subschema')])
diff --git a/tests/test_schema.py b/tests/test_schema.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce8d83fdc827a469aa30aa0f50769fcd0d8de029
--- /dev/null
+++ b/tests/test_schema.py
@@ -0,0 +1,399 @@
+import unittest
+import datetime
+
+import ldapserver
+from ldapserver.schema.types import OIDDict, Schema
+from ldapserver.schema import syntaxes, matching_rules
+
+class TestOIDDict(unittest.TestCase):
+	def test_lookup(self):
+		class TestObj:
+			pass
+		oiddict = OIDDict()
+		obj1 = TestObj()
+		oiddict._register(obj1, '1.1.0', '1.1.0')
+		obj2 = TestObj()
+		oiddict._register(obj2, '1.1.1', 'fooBar', 'foo', 'bar')
+		obj3 = TestObj()
+		oiddict._register(obj3, '1.1.2', 'test', 'test')
+		self.assertIs(oiddict[obj1], obj1)
+		self.assertIs(oiddict['1.1.0'], obj1)
+		self.assertIs(oiddict[obj2], obj2)
+		self.assertIs(oiddict['1.1.1'], obj2)
+		self.assertIs(oiddict['fooBar'], obj2)
+		self.assertIs(oiddict['foobar'], obj2)
+		self.assertIs(oiddict['foo'], obj2)
+		self.assertIs(oiddict['bar'], obj2)
+		self.assertIs(oiddict[obj3], obj3)
+		self.assertIs(oiddict['1.1.2'], obj3)
+		self.assertIs(oiddict['test'], obj3)
+		self.assertEqual(len(oiddict), 3)
+		self.assertEqual(set(oiddict), {'1.1.0', 'fooBar', 'test'})
+		self.assertIs(oiddict.get_numeric_oid(obj1), '1.1.0')
+		self.assertIs(oiddict.get_numeric_oid('1.1.0'), '1.1.0')
+		self.assertIs(oiddict.get_numeric_oid(obj2), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid('1.1.1'), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid('fooBar'), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid('foobar'), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid('foo'), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid('bar'), '1.1.1')
+		self.assertIs(oiddict.get_numeric_oid(obj3), '1.1.2')
+		self.assertIs(oiddict.get_numeric_oid('1.1.2'), '1.1.2')
+		self.assertIs(oiddict.get_numeric_oid('test'), '1.1.2')
+
+	def test_uniqueness(self):
+		class TestObj:
+			pass
+		oiddict = OIDDict()
+		obj1 = TestObj()
+		oiddict._register(obj1, '1.1.0', 'foo', 'bar')
+		# Duplicate registration of the same obj is ok
+		oiddict._register(obj1, '1.1.0', 'foo', 'bar')
+		obj2 = TestObj()
+		# Duplicate registration of another obj with the same name/OID is not ok
+		with self.assertRaises(Exception):
+			oiddict._register(obj2, '1.1.0', 'fooBar')
+		with self.assertRaises(Exception):
+			oiddict._register(obj2, '1.1.1', 'fooBar', 'foo', 'test')
+
+class TestSchema(unittest.TestCase):
+	def test_syntax_registration(self):
+		schema = Schema(syntax_definitions=[syntaxes.DirectoryString])
+		self.assertEqual(len(schema), 1)
+		self.assertEqual(len(schema.syntaxes), 1)
+		self.assertIn(syntaxes.DirectoryString.oid, schema)
+		self.assertIn(syntaxes.DirectoryString.oid, schema.syntaxes)
+		syntax = schema[syntaxes.DirectoryString.oid]
+		self.assertIs(syntax.schema, schema)
+		self.assertEqual(syntax.definition, syntaxes.DirectoryString)
+		self.assertEqual(syntax.oid, syntaxes.DirectoryString.oid)
+		self.assertEqual(syntax.ref, syntaxes.DirectoryString.oid)
+		self.assertEqual(syntax.compatible_matching_rules, set())
+
+	def test_matching_rule_registration(self):
+		syntax_definitions = [
+			syntaxes.DirectoryString,
+			syntaxes.TelephoneNumber,
+		]
+		matching_rule_definitions = [
+			matching_rules.caseExactMatch,
+			matching_rules.telephoneNumberMatch,
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions)
+		self.assertEqual(len(schema), 4)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 2)
+		self.assertIn(matching_rules.caseExactMatch.oid, schema)
+		self.assertIn(matching_rules.caseExactMatch.oid, schema.matching_rules)
+		for name in matching_rules.caseExactMatch.name:
+			self.assertIn(name, schema.matching_rules)
+		matching_rule = schema[matching_rules.caseExactMatch.oid]
+		self.assertIs(matching_rule.schema, schema)
+		self.assertEqual(matching_rule.definition, matching_rules.caseExactMatch)
+		self.assertEqual(matching_rule.oid, matching_rules.caseExactMatch.oid)
+		self.assertIs(matching_rule.syntax, schema[syntaxes.DirectoryString.oid])
+		self.assertEqual(matching_rule.names, matching_rules.caseExactMatch.name)
+		self.assertEqual(matching_rule.ref, matching_rules.caseExactMatch.name[0])
+		self.assertEqual(matching_rule.compatible_syntaxes, {schema[syntaxes.TelephoneNumber.oid],
+		                                                     schema[syntaxes.DirectoryString.oid]})
+		self.assertEqual(schema[syntaxes.TelephoneNumber.oid].compatible_matching_rules, {matching_rule, schema['telephoneNumberMatch']})
+		self.assertEqual(schema[syntaxes.DirectoryString.oid].compatible_matching_rules, {matching_rule})
+
+	def test_matching_rule_registration_unmet_deps(self):
+		syntax_definitions = [
+			syntaxes.TelephoneNumber,
+		]
+		matching_rule_definitions = [
+			matching_rules.caseExactMatch,
+		]
+		with self.assertRaises(Exception):
+			schema = Schema(syntax_definitions=syntax_definitions,
+			                matching_rule_definitions=matching_rule_definitions)
+
+	def test_attribute_type_registration(self):
+		syntax_definitions = [
+			syntaxes.DirectoryString,
+			syntaxes.SubstringAssertion,
+		]
+		matching_rule_definitions = [
+			matching_rules.caseIgnoreMatch,
+			matching_rules.caseIgnoreSubstringsMatch,
+			matching_rules.caseExactMatch,
+		]
+		attribute_type_definitions = [
+			"( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+			"( 2.5.4.3 NAME ( 'cn' 'commonName' ) SUP name )",
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions,
+		                attribute_type_definitions=attribute_type_definitions)
+		self.assertEqual(len(schema), 7)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 3)
+		self.assertEqual(len(schema.attribute_types), 2)
+		for name in ['2.5.4.3', 'cn', 'commonName']:
+			self.assertIn(name, schema)
+			self.assertIn(name, schema.attribute_types)
+		attribute_type = schema['cn']
+		self.assertIs(attribute_type.schema, schema)
+		self.assertEqual(attribute_type.oid, '2.5.4.3')
+		self.assertEqual(attribute_type.names, ['cn', 'commonName'])
+		self.assertEqual(attribute_type.ref, 'cn')
+		self.assertEqual(attribute_type.sup, schema['name'])
+		self.assertEqual(attribute_type.subtypes, set())
+		self.assertEqual(schema['name'].subtypes, {attribute_type})
+		self.assertEqual(attribute_type.equality, schema['caseIgnoreMatch'])
+		self.assertIsNone(attribute_type.ordering)
+		self.assertEqual(attribute_type.substr, schema['caseIgnoreSubstringsMatch'])
+		self.assertFalse(attribute_type.is_operational)
+		self.assertIn(attribute_type, schema.user_attribute_types)
+		self.assertEqual(attribute_type.compatible_matching_rules,
+		                 {schema['caseIgnoreMatch'], schema['caseIgnoreSubstringsMatch'], schema['caseExactMatch']})
+		self.assertIn(attribute_type, schema['caseIgnoreMatch'].compatible_attribute_types)
+		self.assertIn(attribute_type, schema['caseIgnoreSubstringsMatch'].compatible_attribute_types)
+		self.assertIn(attribute_type, schema['caseExactMatch'].compatible_attribute_types)
+
+	def test_attribute_type_registration_wrong_order(self):
+		syntax_definitions = [
+			syntaxes.DirectoryString,
+			syntaxes.SubstringAssertion,
+		]
+		matching_rule_definitions = [
+			matching_rules.caseIgnoreMatch,
+			matching_rules.caseIgnoreSubstringsMatch,
+			matching_rules.caseExactMatch,
+		]
+		attribute_type_definitions = [
+			"( 2.5.4.3 NAME ( 'cn' 'commonName' ) SUP name )",
+			"( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions,
+		                attribute_type_definitions=attribute_type_definitions)
+		self.assertEqual(len(schema), 7)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 3)
+		self.assertEqual(len(schema.attribute_types), 2)
+
+	def test_object_class_registration(self):
+		syntax_definitions = [
+			syntaxes.DN,
+			syntaxes.OID,
+		]
+		matching_rule_definitions = [
+			matching_rules.distinguishedNameMatch,
+			matching_rules.objectIdentifierMatch,
+		]
+		attribute_type_definitions = [
+			"( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )",
+			"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+		]
+		object_class_definitions = [
+			"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+			"( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )",
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions,
+		                attribute_type_definitions=attribute_type_definitions,
+		                object_class_definitions=object_class_definitions)
+		self.assertEqual(len(schema), 8)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 2)
+		self.assertEqual(len(schema.attribute_types), 2)
+		self.assertEqual(len(schema.object_classes), 2)
+
+	def test_object_class_registration_wrong_order(self):
+		syntax_definitions = [
+			syntaxes.DN,
+			syntaxes.OID,
+		]
+		matching_rule_definitions = [
+			matching_rules.distinguishedNameMatch,
+			matching_rules.objectIdentifierMatch,
+		]
+		attribute_type_definitions = [
+			"( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )",
+			"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+		]
+		object_class_definitions = [
+			"( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )",
+			"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions,
+		                attribute_type_definitions=attribute_type_definitions,
+		                object_class_definitions=object_class_definitions)
+		self.assertEqual(len(schema), 8)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 2)
+		self.assertEqual(len(schema.attribute_types), 2)
+		self.assertEqual(len(schema.object_classes), 2)
+
+	def test_object_extend(self):
+		syntax_definitions = [
+			syntaxes.DN,
+			syntaxes.OID,
+		]
+		matching_rule_definitions = [
+			matching_rules.distinguishedNameMatch,
+			matching_rules.objectIdentifierMatch,
+		]
+		attribute_type_definitions = [
+			"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+		]
+		object_class_definitions = [
+			"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+		]
+		schema = Schema(syntax_definitions=syntax_definitions,
+		                matching_rule_definitions=matching_rule_definitions,
+		                attribute_type_definitions=attribute_type_definitions,
+		                object_class_definitions=object_class_definitions)
+		self.assertEqual(len(schema), 6)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 2)
+		self.assertEqual(len(schema.attribute_types), 1)
+		self.assertEqual(len(schema.object_classes), 1)
+		attribute_type_definitions = [
+			"( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )",
+		]
+		object_class_definitions = [
+			"( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )",
+		]
+		schema = schema.extend(attribute_type_definitions=attribute_type_definitions,
+		                       object_class_definitions=object_class_definitions)
+		self.assertEqual(len(schema), 8)
+		self.assertEqual(len(schema.syntaxes), 2)
+		self.assertEqual(len(schema.matching_rules), 2)
+		self.assertEqual(len(schema.attribute_types), 2)
+		self.assertEqual(len(schema.object_classes), 2)
+		self.assertEqual(schema['distinguishedNameMatch'].compatible_attribute_types,
+		                 {schema['aliasedObjectName']})
+
+	def test_or(self):
+		syntax_definitions0 = [
+			syntaxes.DN,
+			syntaxes.OID,
+			syntaxes.DirectoryString,
+			syntaxes.SubstringAssertion,
+		]
+		matching_rule_definitions0 = [
+			matching_rules.distinguishedNameMatch,
+			matching_rules.objectIdentifierMatch,
+			matching_rules.caseIgnoreMatch,
+			matching_rules.caseIgnoreSubstringsMatch,
+		]
+		attribute_type_definitions0 = [
+			"( 2.5.4.1 NAME 'aliasedObjectName' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE )",
+			"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+			"( 2.5.4.41 NAME 'name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+		]
+		object_class_definitions0 = [
+			"( 2.5.6.1 NAME 'alias' SUP top STRUCTURAL MUST aliasedObjectName )",
+			"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+		]
+		schema0 = Schema(syntax_definitions=syntax_definitions0,
+		                 matching_rule_definitions=matching_rule_definitions0,
+		                 attribute_type_definitions=attribute_type_definitions0,
+		                 object_class_definitions=object_class_definitions0)
+		syntax_definitions1 = [
+			syntaxes.DirectoryString,
+			syntaxes.OID,
+		]
+		matching_rule_definitions1 = [
+			matching_rules.caseExactMatch,
+			matching_rules.objectIdentifierMatch,
+		]
+		attribute_type_definitions1 = [
+			"( 2.5.4.0 NAME 'objectClass' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )",
+			"( 1.3.6.1.4.1.250.1.57 NAME 'labeledURI' DESC 'Uniform Resource Identifier with optional label' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )",
+		]
+		object_class_definitions1 = [
+			"( 2.5.6.0 NAME 'top' ABSTRACT MUST objectClass )",
+			"( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'object that contains the URI attribute type' SUP top AUXILIARY MAY labeledURI )",
+		]
+		schema1 = Schema(syntax_definitions=syntax_definitions1,
+		                 matching_rule_definitions=matching_rule_definitions1,
+		                 attribute_type_definitions=attribute_type_definitions1,
+		                 object_class_definitions=object_class_definitions1)
+		schema = schema0 | schema1
+		self.assertEqual(len(schema.syntaxes), 4)
+		self.assertEqual(len(schema.matching_rules), 5)
+		self.assertEqual(len(schema.attribute_types), 4)
+		self.assertEqual(len(schema.object_classes), 3)
+
+class TestAttributeType(unittest.TestCase):
+	schema = ldapserver.schema.RFC4519_SCHEMA
+
+	def test_repr(self):
+		self.assertIsInstance(repr(self.schema['cn']), str)
+
+	def test_encode(self):
+		self.assertEqual(self.schema['cn'].encode('foo äöü BAR'), b'foo \xc3\xa4\xc3\xb6\xc3\xbc BAR')
+
+	def test_decode(self):
+		self.assertEqual(self.schema['cn'].decode(b'foo \xc3\xa4\xc3\xb6\xc3\xbc BAR'), 'foo äöü BAR')
+
+	def test_match_equal(self):
+		self.assertFalse(self.schema['cn'].match_equal([], b'test'))
+		self.assertFalse(self.schema['cn'].match_equal(['foo'], b'test'))
+		self.assertTrue(self.schema['cn'].match_equal(['foo', 'bar', 'äöü'], b'\xc3\xa4\xc3\xb6\xc3\xbc '))
+		self.assertTrue(self.schema['cn'].match_equal(['foo', 'bar', 'äöü'], b'BAR'))
+		# objectIdentifierMatch actually uses data from the self.schema
+		self.assertTrue(self.schema['objectclass'].match_equal(['2.5.6.0', 'Alias'], b'2.5.6.1'))
+		self.assertTrue(self.schema['objectclass'].match_equal(['2.5.6.0', 'Alias'], b'tOp'))
+		# 'facsimileTelephoneNumber' has no EQUALITY
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['facsimileTelephoneNumber'].match_equal([b'test'], b'test')
+
+	def test_match_substr(self):
+		self.assertFalse(self.schema['cn'].match_substr([], b'test', [], None))
+		self.assertFalse(self.schema['cn'].match_substr(['foo'], b'test', [], None))
+		self.assertTrue(self.schema['cn'].match_substr(['foo', 'bar', 'äöü'], b'\xc3\xa4', [], None))
+		self.assertTrue(self.schema['cn'].match_substr(['foo', 'bar', 'äöü'], None, [b'BA'], b'r '))
+		# 'facsimileTelephoneNumber' has no SUBSTR
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['facsimileTelephoneNumber'].match_substr([b'test'], b'test', [], None)
+
+	def test_match_approx(self):
+		# We don't have any matching rule that implementes a separate approx match, so ...
+		self.assertFalse(self.schema['cn'].match_approx([], b'test'))
+		self.assertFalse(self.schema['cn'].match_equal(['foo'], b'test'))
+		self.assertTrue(self.schema['cn'].match_approx(['foo', 'bar', 'äöü'], b'\xc3\xa4\xc3\xb6\xc3\xbc '))
+		self.assertTrue(self.schema['cn'].match_approx(['foo', 'bar', 'äöü'], b'BAR'))
+		# objectIdentifierMatch actually uses data from the schema
+		self.assertTrue(self.schema['objectclass'].match_approx(['2.5.6.0', 'Alias'], b'2.5.6.1'))
+		self.assertTrue(self.schema['objectclass'].match_approx(['2.5.6.0', 'Alias'], b'tOp'))
+		# 'facsimileTelephoneNumber' has no EQUALITY
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['facsimileTelephoneNumber'].match_approx([b'test'], b'test')
+
+	def test_match_greater_or_equal(self):
+		self.assertTrue(self.schema['createTimestamp'].match_greater_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700101000140Z'))
+		self.assertTrue(self.schema['createTimestamp'].match_greater_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700101000000Z'))
+		self.assertFalse(self.schema['createTimestamp'].match_greater_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700201000140Z'))
+		# 'cn' has no ORDERING
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['cn'].match_greater_or_equal(['test'], b'test')
+
+	def test_match_less_or_equal(self):
+		self.assertTrue(self.schema['createTimestamp'].match_less_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700101000140Z'))
+		self.assertFalse(self.schema['createTimestamp'].match_less_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700101000000Z'))
+		self.assertTrue(self.schema['createTimestamp'].match_less_or_equal([datetime.datetime.fromtimestamp(100, datetime.timezone.utc)], b'19700201000140Z'))
+		# 'cn' has no ORDERING, but <= is a hybrid of ORDERING and EQUALITY
+		self.schema['cn'].match_less_or_equal(['test'], b'test')
+		# 'facsimileTelephoneNumber' has no EQUALITY/ORDERING
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['facsimileTelephoneNumber'].match_less_or_equal([b'test'], b'test')
+
+	def test_match_extensible(self):
+		self.assertTrue(self.schema['cn'].match_extensible(['test'], b'Test', None))
+		self.assertTrue(self.schema['cn'].match_extensible(['test'], b'Test', self.schema['caseIgnoreMatch']))
+		self.assertFalse(self.schema['cn'].match_extensible(['test'], b'Test', self.schema['caseExactMatch']))
+		self.assertTrue(self.schema['cn'].match_extensible(['test'], b'test', self.schema['caseExactMatch']))
+		# 'facsimileTelephoneNumber' has no EQUALITY (with match_extensible defaults to)
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['facsimileTelephoneNumber'].match_extensible([b'test'], b'test', None)
+		# Incompatible matching
+		with self.assertRaises(ldapserver.exceptions.LDAPInappropriateMatching):
+			self.schema['cn'].match_extensible([b'test'], b'7', self.schema['integerMatch'])
diff --git a/tests/test_schema_definitions.py b/tests/test_schema_definitions.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a998b00a680d2151a300e838b1a9228cb73c307
--- /dev/null
+++ b/tests/test_schema_definitions.py
@@ -0,0 +1,477 @@
+import unittest
+
+import ldapserver
+from ldapserver.schema.definitions import SyntaxDefinition, MatchingRuleDefinition, MatchingRuleKind
+
+class TestSyntaxDefinition(unittest.TestCase):
+	def test_str(self):
+		self.assertEqual(str(SyntaxDefinition('1.2.3.4')), "( 1.2.3.4 )")
+		self.assertEqual(str(SyntaxDefinition('1.2.3.4', desc="")), "( 1.2.3.4 )")
+		self.assertEqual(str(SyntaxDefinition('1.2.3.4', desc="foo bar")),
+		                 "( 1.2.3.4 DESC 'foo bar' )")
+		self.assertEqual(str(SyntaxDefinition('1.2.3.4', desc="foo's bar")),
+		                 "( 1.2.3.4 DESC 'foo\\27s bar' )")
+		self.assertEqual(str(SyntaxDefinition('1.2.3.4', desc="foobar", extensions={'X-FOO': ['foo bar', 'foobar']})),
+		                 "( 1.2.3.4 DESC 'foobar' X-FOO ( 'foo bar' 'foobar' ) )")
+
+	def test_first_component_oid(self):
+		self.assertEqual(SyntaxDefinition('1.2.3.4').oid, '1.2.3.4')
+
+	def test_compatability_tags(self):
+		self.assertEqual(SyntaxDefinition('1.2.3.4').compatability_tags, {'1.2.3.4'})
+		self.assertEqual(SyntaxDefinition('1.2.3.4', extra_compatability_tags={'4.3.2.1', 'foo'}).compatability_tags, {'1.2.3.4', '4.3.2.1', 'foo'})
+
+syntax = ldapserver.schema.syntaxes.DirectoryString
+
+class TestMatchingRuleDefinition(unittest.TestCase):
+	def test_str(self):
+		self.assertEqual(str(MatchingRuleDefinition('1.2.3.4', name=['fooBarMatch'], desc="Matching rule's description", obsolete=True, syntax='1.3.6.1.4.1.1466.115.121.1.15', extensions={'X-FOO': ['foo bar', 'foobar']}, kind=MatchingRuleKind.EQUALITY)),
+		                 "( 1.2.3.4 NAME 'fooBarMatch' DESC 'Matching rule\\27s description' OBSOLETE SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-FOO ( 'foo bar' 'foobar' ) )")
+		self.assertEqual(str(MatchingRuleDefinition('1.2.3.4', name=['fooMatch', 'fooBarMatch'], syntax='1.3.6.1.4.1.1466.115.121.1.15', kind=MatchingRuleKind.EQUALITY)),
+		                 "( 1.2.3.4 NAME ( 'fooMatch' 'fooBarMatch' ) SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )")
+
+	def test_first_component_oid(self):
+		self.assertEqual(MatchingRuleDefinition('1.2.3.4', syntax='1.3.6.1.4.1.1466.115.121.1.15', kind=MatchingRuleKind.EQUALITY).first_component_oid, '1.2.3.4')
+
+	def test_compatability_tags(self):
+		self.assertEqual(MatchingRuleDefinition('1.2.3.4', syntax='1.3.6.1.4.1.1466.115.121.1.15', kind=MatchingRuleKind.EQUALITY).compatability_tag, '1.3.6.1.4.1.1466.115.121.1.15')
+		self.assertEqual(MatchingRuleDefinition('1.2.3.4', syntax='1.3.6.1.4.1.1466.115.121.1.15', compatability_tag='foo', kind=MatchingRuleKind.EQUALITY).compatability_tag, 'foo')
+
+class TestAttributeTypeDefinition(unittest.TestCase):
+	pass # TODO
+
+class TestObjectClassDefinition(unittest.TestCase):
+	pass # TODO
+
+class TestBuiltin(unittest.TestCase):
+	def test_encoding(self):
+		schemas = [
+			ldapserver.schema.RFC4512_SCHEMA,
+			ldapserver.schema.RFC4519_SCHEMA,
+			ldapserver.schema.RFC4523_SCHEMA,
+			ldapserver.schema.RFC4524_SCHEMA,
+			ldapserver.schema.RFC3112_SCHEMA,
+			ldapserver.schema.RFC2079_SCHEMA,
+			ldapserver.schema.RFC2798_SCHEMA,
+			ldapserver.schema.RFC2307BIS_SCHEMA,
+		]
+		for schema in schemas:
+			for obj in schema.syntax_definitions:
+				str(obj)
+			for obj in schema.matching_rule_definitions:
+				str(obj)
+			for obj in schema.attribute_type_definitions:
+				str(obj)
+			for obj in schema.object_class_definitions:
+				str(obj)
+
+# Test correctness and completeness of bundled schemas based on IANA registry
+class TestOIDs(unittest.TestCase):
+	def test_core(self):
+		schema = ldapserver.schema.RFC4512_SCHEMA
+		# Matching rules (RFC4517)
+		self.assertIn('bitStringMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['bitStringMatch'].oid, '2.5.13.16')
+		self.assertIn('booleanMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['booleanMatch'].oid, '2.5.13.13')
+		self.assertIn('caseExactIA5Match', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseExactIA5Match'].oid, '1.3.6.1.4.1.1466.109.114.1')
+		self.assertIn('caseExactMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseExactMatch'].oid, '2.5.13.5')
+		self.assertIn('caseExactOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseExactOrderingMatch'].oid, '2.5.13.6')
+		self.assertIn('caseExactSubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseExactSubstringsMatch'].oid, '2.5.13.7')
+		self.assertIn('caseIgnoreIA5Match', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreIA5Match'].oid, '1.3.6.1.4.1.1466.109.114.2')
+		self.assertIn('caseIgnoreIA5SubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreIA5SubstringsMatch'].oid, '1.3.6.1.4.1.1466.109.114.3')
+		self.assertIn('caseIgnoreListMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreListMatch'].oid, '2.5.13.11')
+		self.assertIn('caseIgnoreListSubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreListSubstringsMatch'].oid, '2.5.13.12')
+		self.assertIn('caseIgnoreMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreMatch'].oid, '2.5.13.2')
+		self.assertIn('caseIgnoreOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreOrderingMatch'].oid, '2.5.13.3')
+		self.assertIn('caseIgnoreSubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['caseIgnoreSubstringsMatch'].oid, '2.5.13.4')
+		self.assertIn('directoryStringFirstComponentMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['directoryStringFirstComponentMatch'].oid, '2.5.13.31')
+		self.assertIn('distinguishedNameMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['distinguishedNameMatch'].oid, '2.5.13.1')
+		self.assertIn('generalizedTimeMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['generalizedTimeMatch'].oid, '2.5.13.27')
+		self.assertIn('generalizedTimeOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['generalizedTimeOrderingMatch'].oid, '2.5.13.28')
+		self.assertIn('integerFirstComponentMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['integerFirstComponentMatch'].oid, '2.5.13.29')
+		self.assertIn('integerMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['integerMatch'].oid, '2.5.13.14')
+		self.assertIn('integerOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['integerOrderingMatch'].oid, '2.5.13.15')
+		self.assertIn('keywordMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['keywordMatch'].oid, '2.5.13.33')
+		self.assertIn('numericStringMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['numericStringMatch'].oid, '2.5.13.8')
+		self.assertIn('numericStringOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['numericStringOrderingMatch'].oid, '2.5.13.9')
+		self.assertIn('numericStringSubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['numericStringSubstringsMatch'].oid, '2.5.13.10')
+		self.assertIn('objectIdentifierFirstComponentMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['objectIdentifierFirstComponentMatch'].oid, '2.5.13.30')
+		self.assertIn('objectIdentifierMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['objectIdentifierMatch'].oid, '2.5.13.0')
+		self.assertIn('octetStringMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['octetStringMatch'].oid, '2.5.13.17')
+		self.assertIn('octetStringOrderingMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['octetStringOrderingMatch'].oid, '2.5.13.18')
+		self.assertIn('telephoneNumberMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['telephoneNumberMatch'].oid, '2.5.13.20')
+		self.assertIn('telephoneNumberSubstringsMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['telephoneNumberSubstringsMatch'].oid, '2.5.13.21')
+		self.assertIn('uniqueMemberMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['uniqueMemberMatch'].oid, '2.5.13.23')
+		self.assertIn('wordMatch', schema.matching_rules)
+		self.assertEqual(schema.matching_rules['wordMatch'].oid, '2.5.13.32')
+		# Attribute types (RFC4512)
+		self.assertIn('aliasedObjectName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['aliasedObjectName'].oid, '2.5.4.1')
+		self.assertIn('altServer', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['altServer'].oid, '1.3.6.1.4.1.1466.101.120.6')
+		self.assertIn('attributeTypes', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['attributeTypes'].oid, '2.5.21.5')
+		self.assertIn('createTimestamp', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['createTimestamp'].oid, '2.5.18.1')
+		self.assertIn('creatorsName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['creatorsName'].oid, '2.5.18.3')
+		self.assertIn('dITContentRules', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['dITContentRules'].oid, '2.5.21.2')
+		self.assertIn('dITStructureRules', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['dITStructureRules'].oid, '2.5.21.1')
+		self.assertIn('governingStructureRule', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['governingStructureRule'].oid, '2.5.21.10')
+		self.assertIn('ldapSyntaxes', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['ldapSyntaxes'].oid, '1.3.6.1.4.1.1466.101.120.16')
+		self.assertIn('matchingRules', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['matchingRules'].oid, '2.5.21.4')
+		self.assertIn('matchingRuleUse', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['matchingRuleUse'].oid, '2.5.21.8')
+		self.assertIn('modifiersName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['modifiersName'].oid, '2.5.18.4')
+		self.assertIn('modifyTimestamp', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['modifyTimestamp'].oid, '2.5.18.2')
+		self.assertIn('nameForms', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['nameForms'].oid, '2.5.21.7')
+		self.assertIn('namingContexts', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['namingContexts'].oid, '1.3.6.1.4.1.1466.101.120.5')
+		self.assertIn('objectClass', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['objectClass'].oid, '2.5.4.0')
+		self.assertIn('objectClasses', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['objectClasses'].oid, '2.5.21.6')
+		self.assertIn('structuralObjectClass', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['structuralObjectClass'].oid, '2.5.21.9')
+		self.assertIn('subschemaSubentry', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['subschemaSubentry'].oid, '2.5.18.10')
+		self.assertIn('supportedControl', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedControl'].oid, '1.3.6.1.4.1.1466.101.120.13')
+		self.assertIn('supportedExtension', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedExtension'].oid, '1.3.6.1.4.1.1466.101.120.7')
+		self.assertIn('supportedFeatures', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedFeatures'].oid, '1.3.6.1.4.1.4203.1.3.5')
+		self.assertIn('supportedLDAPVersion', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedLDAPVersion'].oid, '1.3.6.1.4.1.1466.101.120.15')
+		self.assertIn('supportedSASLMechanisms', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedSASLMechanisms'].oid, '1.3.6.1.4.1.1466.101.120.14')
+		# Object classes (RFC4512)
+		self.assertIn('alias', schema.object_classes)
+		self.assertEqual(schema.object_classes['alias'].oid, '2.5.6.1')
+		self.assertIn('extensibleObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['extensibleObject'].oid, '1.3.6.1.4.1.1466.101.120.111')
+		self.assertIn('subschema', schema.object_classes)
+		self.assertEqual(schema.object_classes['subschema'].oid, '2.5.20.1')
+		self.assertIn('top', schema.object_classes)
+		self.assertEqual(schema.object_classes['top'].oid, '2.5.6.0')
+
+	def test_rfc4519(self):
+		schema = ldapserver.schema.RFC4519_SCHEMA
+		# Attribute types
+		self.assertIn('businessCategory', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['businessCategory'].oid, '2.5.4.15')
+		self.assertIn('c', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['c'].oid, '2.5.4.6')
+		self.assertIn('cn', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['cn'].oid, '2.5.4.3')
+		self.assertIn('commonName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['commonName'].oid, '2.5.4.3')
+		self.assertIn('countryName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['countryName'].oid, '2.5.4.6')
+		self.assertIn('DC', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['DC'].oid, '0.9.2342.19200300.100.1.25')
+		self.assertIn('description', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['description'].oid, '2.5.4.13')
+		self.assertIn('destinationIndicator', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['destinationIndicator'].oid, '2.5.4.27')
+		self.assertIn('distinguishedName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['distinguishedName'].oid, '2.5.4.49')
+		self.assertIn('dnQualifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['dnQualifier'].oid, '2.5.4.46')
+		self.assertIn('domainComponent', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['domainComponent'].oid, '0.9.2342.19200300.100.1.25')
+		self.assertIn('enhancedSearchGuide', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['enhancedSearchGuide'].oid, '2.5.4.47')
+		self.assertIn('facsimileTelephoneNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['facsimileTelephoneNumber'].oid, '2.5.4.23')
+		self.assertIn('generationQualifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['generationQualifier'].oid, '2.5.4.44')
+		self.assertIn('givenName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['givenName'].oid, '2.5.4.42')
+		self.assertIn('houseIdentifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['houseIdentifier'].oid, '2.5.4.51')
+		self.assertIn('initials', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['initials'].oid, '2.5.4.43')
+		self.assertIn('internationaliSDNNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['internationaliSDNNumber'].oid, '2.5.4.25')
+		self.assertIn('L', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['L'].oid, '2.5.4.7')
+		self.assertIn('localityName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['localityName'].oid, '2.5.4.7')
+		self.assertIn('member', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['member'].oid, '2.5.4.31')
+		self.assertIn('name', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['name'].oid, '2.5.4.41')
+		self.assertIn('o', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['o'].oid, '2.5.4.10')
+		self.assertIn('organizationalUnitName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['organizationalUnitName'].oid, '2.5.4.11')
+		self.assertIn('organizationName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['organizationName'].oid, '2.5.4.10')
+		self.assertIn('ou', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['ou'].oid, '2.5.4.11')
+		self.assertIn('owner', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['owner'].oid, '2.5.4.32')
+		self.assertIn('physicalDeliveryOfficeName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['physicalDeliveryOfficeName'].oid, '2.5.4.19')
+		self.assertIn('postalAddress', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['postalAddress'].oid, '2.5.4.16')
+		self.assertIn('postalCode', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['postalCode'].oid, '2.5.4.17')
+		self.assertIn('postOfficeBox', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['postOfficeBox'].oid, '2.5.4.18')
+		self.assertIn('preferredDeliveryMethod', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['preferredDeliveryMethod'].oid, '2.5.4.28')
+		self.assertIn('registeredAddress', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['registeredAddress'].oid, '2.5.4.26')
+		self.assertIn('roleOccupant', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['roleOccupant'].oid, '2.5.4.33')
+		self.assertIn('searchGuide', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['searchGuide'].oid, '2.5.4.14')
+		self.assertIn('seeAlso', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['seeAlso'].oid, '2.5.4.34')
+		self.assertIn('serialNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['serialNumber'].oid, '2.5.4.5')
+		self.assertIn('sn', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['sn'].oid, '2.5.4.4')
+		self.assertIn('st', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['st'].oid, '2.5.4.8')
+		self.assertIn('street', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['street'].oid, '2.5.4.9')
+		self.assertIn('surname', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['surname'].oid, '2.5.4.4')
+		self.assertIn('telephoneNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['telephoneNumber'].oid, '2.5.4.20')
+		self.assertIn('teletexTerminalIdentifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['teletexTerminalIdentifier'].oid, '2.5.4.22')
+		self.assertIn('telexNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['telexNumber'].oid, '2.5.4.21')
+		self.assertIn('title', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['title'].oid, '2.5.4.12')
+		self.assertIn('uid', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['uid'].oid, '0.9.2342.19200300.100.1.1')
+		self.assertIn('uniqueMember', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['uniqueMember'].oid, '2.5.4.50')
+		self.assertIn('userId', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userId'].oid, '0.9.2342.19200300.100.1.1')
+		self.assertIn('userPassword', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userPassword'].oid, '2.5.4.35')
+		self.assertIn('x121Address', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['x121Address'].oid, '2.5.4.24')
+		self.assertIn('x500UniqueIdentifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['x500UniqueIdentifier'].oid, '2.5.4.45')
+		# Object classes
+		self.assertIn('applicationProcess', schema.object_classes)
+		self.assertEqual(schema.object_classes['applicationProcess'].oid, '2.5.6.11')
+		self.assertIn('country', schema.object_classes)
+		self.assertEqual(schema.object_classes['country'].oid, '2.5.6.2')
+		self.assertIn('dcObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['dcObject'].oid, '1.3.6.1.4.1.1466.344')
+		self.assertIn('device', schema.object_classes)
+		self.assertEqual(schema.object_classes['device'].oid, '2.5.6.14')
+		self.assertIn('groupOfNames', schema.object_classes)
+		self.assertEqual(schema.object_classes['groupOfNames'].oid, '2.5.6.9')
+		self.assertIn('groupOfUniqueNames', schema.object_classes)
+		self.assertEqual(schema.object_classes['groupOfUniqueNames'].oid, '2.5.6.17')
+		self.assertIn('locality', schema.object_classes)
+		self.assertEqual(schema.object_classes['locality'].oid, '2.5.6.3')
+		self.assertIn('organization', schema.object_classes)
+		self.assertEqual(schema.object_classes['organization'].oid, '2.5.6.4')
+		self.assertIn('organizationalPerson', schema.object_classes)
+		self.assertEqual(schema.object_classes['organizationalPerson'].oid, '2.5.6.7')
+		self.assertIn('organizationalRole', schema.object_classes)
+		self.assertEqual(schema.object_classes['organizationalRole'].oid, '2.5.6.8')
+		self.assertIn('organizationalUnit', schema.object_classes)
+		self.assertEqual(schema.object_classes['organizationalUnit'].oid, '2.5.6.5')
+		self.assertIn('person', schema.object_classes)
+		self.assertEqual(schema.object_classes['person'].oid, '2.5.6.6')
+		self.assertIn('residentialPerson', schema.object_classes)
+		self.assertEqual(schema.object_classes['residentialPerson'].oid, '2.5.6.10')
+		self.assertIn('uidObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['uidObject'].oid, '1.3.6.1.1.3.1')
+
+	def test_rfc4523(self):
+		schema = ldapserver.schema.RFC4523_SCHEMA
+		# Attribute types
+		self.assertIn('authorityRevocationList', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['authorityRevocationList'].oid, '2.5.4.38')
+		self.assertIn('cACertificate', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['cACertificate'].oid, '2.5.4.37')
+		self.assertIn('certificateRevocationList', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['certificateRevocationList'].oid, '2.5.4.39')
+		self.assertIn('crossCertificatePair', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['crossCertificatePair'].oid, '2.5.4.40')
+		self.assertIn('deltaRevocationList', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['deltaRevocationList'].oid, '2.5.4.53')
+		self.assertIn('supportedAlgorithms', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['supportedAlgorithms'].oid, '2.5.4.52')
+		self.assertIn('userCertificate', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userCertificate'].oid, '2.5.4.36')
+		# Object class
+		self.assertIn('certificationAuthority', schema.object_classes)
+		self.assertEqual(schema.object_classes['certificationAuthority'].oid, '2.5.6.16')
+		self.assertIn('certificationAuthority-V2', schema.object_classes)
+		self.assertEqual(schema.object_classes['certificationAuthority-V2'].oid, '2.5.6.16.2')
+		self.assertIn('cRLDistributionPoint', schema.object_classes)
+		self.assertEqual(schema.object_classes['cRLDistributionPoint'].oid, '2.5.6.19')
+		self.assertIn('deltaCRL', schema.object_classes)
+		self.assertEqual(schema.object_classes['deltaCRL'].oid, '2.5.6.23')
+		self.assertIn('pkiCA', schema.object_classes)
+		self.assertEqual(schema.object_classes['pkiCA'].oid, '2.5.6.22')
+		self.assertIn('pkiUser', schema.object_classes)
+		self.assertEqual(schema.object_classes['pkiUser'].oid, '2.5.6.21')
+		self.assertIn('strongAuthenticationUser', schema.object_classes)
+		self.assertEqual(schema.object_classes['strongAuthenticationUser'].oid, '2.5.6.15')
+		self.assertIn('userSecurityInformation', schema.object_classes)
+		self.assertEqual(schema.object_classes['userSecurityInformation'].oid, '2.5.6.18')
+
+	def test_rfc4524(self):
+		schema = ldapserver.schema.RFC4524_SCHEMA
+		# Attribute types
+		self.assertIn('associatedDomain', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['associatedDomain'].oid, '0.9.2342.19200300.100.1.37')
+		self.assertIn('associatedName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['associatedName'].oid, '0.9.2342.19200300.100.1.38')
+		self.assertIn('buildingName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['buildingName'].oid, '0.9.2342.19200300.100.1.48')
+		self.assertIn('co', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['co'].oid, '0.9.2342.19200300.100.1.43')
+		self.assertIn('documentAuthor', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentAuthor'].oid, '0.9.2342.19200300.100.1.14')
+		self.assertIn('documentIdentifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentIdentifier'].oid, '0.9.2342.19200300.100.1.11')
+		self.assertIn('documentLocation', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentLocation'].oid, '0.9.2342.19200300.100.1.15')
+		self.assertIn('documentPublisher', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentPublisher'].oid, '0.9.2342.19200300.100.1.56')
+		self.assertIn('documentTitle', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentTitle'].oid, '0.9.2342.19200300.100.1.12')
+		self.assertIn('documentVersion', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['documentVersion'].oid, '0.9.2342.19200300.100.1.13')
+		self.assertIn('drink', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['drink'].oid, '0.9.2342.19200300.100.1.5')
+		self.assertIn('homePhone', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['homePhone'].oid, '0.9.2342.19200300.100.1.20')
+		self.assertIn('homePostalAddress', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['homePostalAddress'].oid, '0.9.2342.19200300.100.1.39')
+		self.assertIn('host', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['host'].oid, '0.9.2342.19200300.100.1.9')
+		self.assertIn('info', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['info'].oid, '0.9.2342.19200300.100.1.4')
+		self.assertIn('mail', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['mail'].oid, '0.9.2342.19200300.100.1.3')
+		self.assertIn('manager', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['manager'].oid, '0.9.2342.19200300.100.1.10')
+		self.assertIn('mobile', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['mobile'].oid, '0.9.2342.19200300.100.1.41')
+		self.assertIn('organizationalStatus', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['organizationalStatus'].oid, '0.9.2342.19200300.100.1.45')
+		self.assertIn('pager', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['pager'].oid, '0.9.2342.19200300.100.1.42')
+		self.assertIn('personalTitle', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['personalTitle'].oid, '0.9.2342.19200300.100.1.40')
+		self.assertIn('roomNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['roomNumber'].oid, '0.9.2342.19200300.100.1.6')
+		self.assertIn('secretary', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['secretary'].oid, '0.9.2342.19200300.100.1.21')
+		# RFC4524 does not define singleLevelQuality
+		#self.assertIn('singleLevelQuality', schema.attribute_types)
+		#self.assertEqual(schema.attribute_types['singleLevelQuality'].oid, '0.9.2342.19200300.100.1.50')
+		self.assertIn('uniqueIdentifier', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['uniqueIdentifier'].oid, '0.9.2342.19200300.100.1.44')
+		self.assertIn('userClass', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userClass'].oid, '0.9.2342.19200300.100.1.8')
+		# Object class
+		self.assertIn('account', schema.object_classes)
+		self.assertEqual(schema.object_classes['account'].oid, '0.9.2342.19200300.100.4.5')
+		self.assertIn('document', schema.object_classes)
+		self.assertEqual(schema.object_classes['document'].oid, '0.9.2342.19200300.100.4.6')
+		# RFC4524 is inconsistent regarding RFC4524:
+		# - It defines the attribute type with 0.9.2342.19200300.100.4.8
+		# - It updates the IANA registration with 0.9.2342.19200300.100.4.9
+		self.assertIn('documentSeries', schema.object_classes)
+		self.assertEqual(schema.object_classes['documentSeries'].oid, '0.9.2342.19200300.100.4.9')
+		self.assertIn('domain', schema.object_classes)
+		self.assertEqual(schema.object_classes['domain'].oid, '0.9.2342.19200300.100.4.13')
+		self.assertIn('domainRelatedObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['domainRelatedObject'].oid, '0.9.2342.19200300.100.4.17')
+		self.assertIn('friendlyCountry', schema.object_classes)
+		self.assertEqual(schema.object_classes['friendlyCountry'].oid, '0.9.2342.19200300.100.4.18')
+		self.assertIn('RFC822LocalPart', schema.object_classes)
+		self.assertEqual(schema.object_classes['RFC822LocalPart'].oid, '0.9.2342.19200300.100.4.14')
+		self.assertIn('room', schema.object_classes)
+		self.assertEqual(schema.object_classes['room'].oid, '0.9.2342.19200300.100.4.7')
+		self.assertIn('simpleSecurityObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['simpleSecurityObject'].oid, '0.9.2342.19200300.100.4.19')
+
+	def test_rfc2079(self):
+		schema = ldapserver.schema.RFC2079_SCHEMA
+		# Attribute types
+		self.assertIn('labeledURI', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['labeledURI'].oid, '1.3.6.1.4.1.250.1.57')
+		# Object class
+		self.assertIn('labeledURIObject', schema.object_classes)
+		self.assertEqual(schema.object_classes['labeledURIObject'].oid, '1.3.6.1.4.1.250.3.15')
+
+	def test_rfc2798(self):
+		schema = ldapserver.schema.RFC2798_SCHEMA
+		# Attribute types
+		self.assertIn('carLicense', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['carLicense'].oid, '2.16.840.1.113730.3.1.1')
+		self.assertIn('departmentNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['departmentNumber'].oid, '2.16.840.1.113730.3.1.2')
+		self.assertIn('displayName', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['displayName'].oid, '2.16.840.1.113730.3.1.241')
+		self.assertIn('employeeNumber', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['employeeNumber'].oid, '2.16.840.1.113730.3.1.3')
+		self.assertIn('employeeType', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['employeeType'].oid, '2.16.840.1.113730.3.1.4')
+		self.assertIn('jpegPhoto', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['jpegPhoto'].oid, '0.9.2342.19200300.100.1.60')
+		self.assertIn('preferredLanguage', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['preferredLanguage'].oid, '2.16.840.1.113730.3.1.39')
+		self.assertIn('userPKCS12', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userPKCS12'].oid, '2.16.840.1.113730.3.1.216')
+		self.assertIn('userSMIMECertificate', schema.attribute_types)
+		self.assertEqual(schema.attribute_types['userSMIMECertificate'].oid, '2.16.840.1.113730.3.1.40')
+		# Object class
+		self.assertIn('inetOrgPerson', schema.object_classes)
+		self.assertEqual(schema.object_classes['inetOrgPerson'].oid, '2.16.840.1.113730.3.2.2')
diff --git a/tests/test_schema_matching_rule.py b/tests/test_schema_matching_rule.py
new file mode 100644
index 0000000000000000000000000000000000000000..a740ed0220b9c3548713f2ae2626ee3501479080
--- /dev/null
+++ b/tests/test_schema_matching_rule.py
@@ -0,0 +1,182 @@
+import unittest
+
+import ldapserver
+from ldapserver.schema import matching_rules
+
+class TestGenericEqualityMatchingRule(unittest.TestCase):
+	def test_match_equal(self):
+		rule = matching_rules.integerMatch
+		self.assertTrue(rule.match_equal(None, [1234], 1234))
+		self.assertFalse(rule.match_equal(None, [4321], 1234))
+		self.assertFalse(rule.match_equal(None, [1234], 4321))
+		self.assertTrue(rule.match_equal(None, [0, 1], 0))
+		self.assertTrue(rule.match_equal(None, [0, 1], 1))
+		self.assertFalse(rule.match_equal(None, [0, 1], -1))
+		self.assertFalse(rule.match_equal(None, [0, 1], 2))
+		self.assertFalse(rule.match_equal(None, [], 1))
+
+class TestGenericOrderingMatchingRule(unittest.TestCase):
+	def test_match_less(self):
+		rule = matching_rules.integerOrderingMatch
+		self.assertFalse(rule.match_less(None, [1234], 1234))
+		self.assertFalse(rule.match_less(None, [4321], 1234))
+		self.assertTrue(rule.match_less(None, [1234], 4321))
+		self.assertFalse(rule.match_less(None, [0, 1], 0))
+		self.assertTrue(rule.match_less(None, [0, 1], 1))
+		self.assertFalse(rule.match_less(None, [0, 1], -1))
+		self.assertTrue(rule.match_less(None, [0, 1], 2))
+		self.assertFalse(rule.match_less(None, [], 1))
+
+	def test_match_greater_or_equal(self):
+		rule = matching_rules.integerOrderingMatch
+		self.assertTrue(rule.match_greater_or_equal(None, [1234], 1234))
+		self.assertTrue(rule.match_greater_or_equal(None, [4321], 1234))
+		self.assertFalse(rule.match_greater_or_equal(None, [1234], 4321))
+		self.assertTrue(rule.match_greater_or_equal(None, [0, 1], 0))
+		self.assertTrue(rule.match_greater_or_equal(None, [0, 1], 1))
+		self.assertTrue(rule.match_greater_or_equal(None, [0, 1], -1))
+		self.assertFalse(rule.match_greater_or_equal(None, [0, 1], 2))
+		self.assertFalse(rule.match_greater_or_equal(None, [], 1))
+
+class TestStringEqualityMatchingRule(unittest.TestCase):
+	def test_match_equal(self):
+		rule = matching_rules.caseIgnoreMatch
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'foo'))
+		self.assertFalse(rule.match_equal(None, ['foo', 'Bar'], 'foobar'))
+		self.assertFalse(rule.match_equal(None, [], 'foo'))
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'Bar'))
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'Foo'))
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'bar'))
+		self.assertTrue(rule.match_equal(None, ['fo  o ', ' bar'], '   bar   '))
+		self.assertFalse(rule.match_equal(None, ['fo  o ', ' b ar'], '   bar   '))
+		self.assertTrue(rule.match_equal(None, ['fo\n\ro ', ' bar'], ' fo o'))
+		rule = matching_rules.caseExactMatch
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'foo'))
+		self.assertFalse(rule.match_equal(None, ['foo', 'Bar'], 'foobar'))
+		self.assertFalse(rule.match_equal(None, [], 'foo'))
+		self.assertTrue(rule.match_equal(None, ['foo', 'Bar'], 'Bar'))
+		self.assertFalse(rule.match_equal(None, ['foo', 'Bar'], 'Foo'))
+		self.assertFalse(rule.match_equal(None, ['foo', 'Bar'], 'bar'))
+		self.assertTrue(rule.match_equal(None, ['fo  o ', ' bar'], '   bar   '))
+		self.assertFalse(rule.match_equal(None, ['fo  o ', ' b ar'], '   bar   '))
+		self.assertTrue(rule.match_equal(None, ['fo\n\ro ', ' bar'], ' fo o'))
+		# Systematic tests for stringprep in test_stringprep.py
+
+class TestStringOrderingMatchingRule(unittest.TestCase):
+	def test_match_less(self):
+		rule = matching_rules.caseIgnoreOrderingMatch
+		self.assertFalse(rule.match_less(None, ['abc'], 'abc'))
+		self.assertFalse(rule.match_less(None, [], 'abc'))
+		self.assertTrue(rule.match_less(None, ['abc'], 'def'))
+		self.assertTrue(rule.match_less(None, ['abc'], 'acd'))
+		self.assertTrue(rule.match_less(None, ['def', 'abc'], 'acd'))
+		self.assertTrue(rule.match_less(None, ['A'], 'b'))
+		self.assertFalse(rule.match_less(None, ['a'], 'A'))
+		self.assertFalse(rule.match_less(None, ['C'], 'a'))
+		rule = matching_rules.caseExactOrderingMatch
+		self.assertFalse(rule.match_less(None, ['abc'], 'abc'))
+		self.assertFalse(rule.match_less(None, [], 'abc'))
+		self.assertTrue(rule.match_less(None, ['abc'], 'def'))
+		self.assertTrue(rule.match_less(None, ['abc'], 'acd'))
+		self.assertTrue(rule.match_less(None, ['def', 'abc'], 'acd'))
+		self.assertTrue(rule.match_less(None, ['A'], 'b'))
+		self.assertFalse(rule.match_less(None, ['a'], 'A'))
+		self.assertTrue(rule.match_less(None, ['C'], 'a'))
+		# Systematic tests for stringprep in test_stringprep.py
+
+	def test_match_greater_or_equal(self):
+		rule = matching_rules.caseIgnoreOrderingMatch
+		self.assertTrue(rule.match_greater_or_equal(None, ['abc'], 'abc'))
+		self.assertFalse(rule.match_greater_or_equal(None, [], 'abc'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['abc'], 'def'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['abc'], 'acd'))
+		self.assertTrue(rule.match_greater_or_equal(None, ['def', 'abc'], 'acd'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['A'], 'b'))
+		self.assertTrue(rule.match_greater_or_equal(None, ['a'], 'A'))
+		self.assertTrue(rule.match_greater_or_equal(None, ['C'], 'a'))
+		rule = matching_rules.caseExactOrderingMatch
+		self.assertTrue(rule.match_greater_or_equal(None, ['abc'], 'abc'))
+		self.assertFalse(rule.match_greater_or_equal(None, [], 'abc'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['abc'], 'def'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['abc'], 'acd'))
+		self.assertTrue(rule.match_greater_or_equal(None, ['def', 'abc'], 'acd'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['A'], 'b'))
+		self.assertTrue(rule.match_greater_or_equal(None, ['a'], 'A'))
+		self.assertFalse(rule.match_greater_or_equal(None, ['C'], 'a'))
+		# Systematic tests for stringprep in test_stringprep.py
+
+class TestStringSubstrMatchingRule(unittest.TestCase):
+	def test_match_substr(self):
+		rule = matching_rules.caseExactSubstringsMatch
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abcdefghi', [], None))
+		self.assertTrue(rule.match_substr(None, ['foo', 'abcdefghi', 'bar'], 'abcdefghi', [], None))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], None, ['abcdefghi'], None))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], None, [], 'abcdefghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abc', ['def'], 'ghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abc', ['d', 'ef'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abcd', ['d', 'ef'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abc', ['cd', 'ef'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abc', ['de', 'ef'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abc', ['d', 'def'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abc', ['d', 'efg'], 'ghi'))
+		self.assertFalse(rule.match_substr(None, ['abcdefghi'], 'abc', ['d', 'ef'], 'fghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'ab', ['def'], 'ghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abc', ['ef'], 'ghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abc', ['de'], 'ghi'))
+		self.assertTrue(rule.match_substr(None, ['abcdefghi'], 'abc', ['def'], 'hi'))
+		# TODO: more systematic tests
+
+class TestStringListEqualityMatchingRule(unittest.TestCase):
+	def test_equal(self):
+		rule = matching_rules.caseIgnoreListMatch
+		self.assertFalse(rule.match_equal(None, [], ['foo', 'bar']))
+		self.assertTrue(rule.match_equal(None, [['foo', 'bar']], ['foo', 'bar']))
+		self.assertTrue(rule.match_equal(None, [['Foo', 'bar']], ['foo', 'BAR']))
+		self.assertFalse(rule.match_equal(None, [['bar', 'foo']], ['foo', 'bar']))
+		self.assertFalse(rule.match_equal(None, [['foo', 'bar']], ['foo']))
+		self.assertTrue(rule.match_equal(None, [['first'], ['foo', 'bar']], ['foo', 'bar']))
+		self.assertTrue(rule.match_equal(None, [['line'], ['foo', 'bar']], ['line']))
+
+class TestStringListSubstrMatchingRule(unittest.TestCase):
+	def test_match_substr(self):
+		rule = matching_rules.caseIgnoreListSubstringsMatch
+		self.assertFalse(rule.match_substr(None, [], None, ['foo'], None))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], 'foo', ['bar'], 'baz'))
+		self.assertFalse(rule.match_substr(None, [['foo', 'bar', 'baz']], 'bar', [], None))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], 'FOO', [], None))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], None, ['bar'], 'baz'))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], 'foo', [], 'baz'))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], 'foo', ['bar'], None))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], None, ['foo', 'bar', 'baz'], None))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar', 'baz']], 'f', ['b', 'r'], 'z'))
+		self.assertTrue(rule.match_substr(None, [['foo', 'bar']], None, ['foo', 'bar'], None))
+		self.assertFalse(rule.match_substr(None, [['foo', 'bar']], None, ['foobar'], None))
+		self.assertFalse(rule.match_substr(None, [['foo', 'bar']], None, ['foo bar'], None))
+		# LF is internally used as a separator
+		self.assertFalse(rule.match_substr(None, [['foo', 'bar']], None, ['foo\nbar'], None))
+
+class TestFirstComponentMatchingRule(unittest.TestCase):
+	def test_equal(self):
+		class FirstCompontentIntegerValue:
+			def __init__(self, integer):
+				self.first_component_integer = integer
+		rule = matching_rules.integerFirstComponentMatch
+		self.assertTrue(rule.match_equal(None, [FirstCompontentIntegerValue(0), FirstCompontentIntegerValue(1)], 0))
+		self.assertTrue(rule.match_equal(None, [FirstCompontentIntegerValue(0), FirstCompontentIntegerValue(1)], 1))
+		self.assertFalse(rule.match_equal(None, [FirstCompontentIntegerValue(0), FirstCompontentIntegerValue(1)], 3))
+		self.assertFalse(rule.match_equal(None, [], 1))
+
+class TestOIDMatchingRule(unittest.TestCase):
+	def test_equal(self):
+		schema = ldapserver.schema.RFC4519_SCHEMA
+		rule = matching_rules.objectIdentifierMatch
+		self.assertTrue(rule.match_equal(schema, ['person', '2.5.6.2'], '2.5.6.6'))
+		self.assertTrue(rule.match_equal(schema, ['person', '2.5.6.2'], 'Country'))
+		self.assertFalse(rule.match_equal(schema, [], '2.5.6.6'))
+		self.assertFalse(rule.match_equal(schema, [], 'Country'))
+		self.assertTrue(rule.match_equal(schema, ['person', 'foobar'], 'person'))
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			rule.match_equal(schema, ['person'], 'foobar')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			rule.match_equal(schema, ['person', 'foobar'], 'foobar')
+		self.assertTrue(rule.match_equal(schema, ['person', '0.1.2.3.4'], '0.1.2.3.4'))
diff --git a/tests/test_schema_syntaxes.py b/tests/test_schema_syntaxes.py
new file mode 100644
index 0000000000000000000000000000000000000000..955481f7046d9bdb81d22bd7159260fbededb0ee
--- /dev/null
+++ b/tests/test_schema_syntaxes.py
@@ -0,0 +1,171 @@
+import unittest
+import datetime
+
+import ldapserver
+from ldapserver.schema import syntaxes
+
+class TestBytesSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.OctetString
+		self.assertEqual(syntax.encode(None, b'Foo'), b'Foo')
+
+	def test_decode(self):
+		syntax = syntaxes.OctetString
+		self.assertEqual(syntax.decode(None, b'Foo'), b'Foo')
+
+class TestStringSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.DirectoryString
+		self.assertEqual(syntax.encode(None, 'Foo'), b'Foo')
+		self.assertEqual(syntax.encode(None, 'äöü'), b'\xc3\xa4\xc3\xb6\xc3\xbc')
+
+	def test_decode(self):
+		syntax = syntaxes.DirectoryString
+		self.assertEqual(syntax.decode(None, b'Foo'), 'Foo')
+		self.assertEqual(syntax.decode(None, b'\xc3\xa4\xc3\xb6\xc3\xbc'), 'äöü')
+		syntax = syntaxes.IA5String
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'\xc3\xa4\xc3\xb6\xc3\xbc')
+		# Test regex matching
+		syntax = syntaxes.BitString
+		self.assertEqual(syntax.decode(None, b"''B"), "''B")
+		self.assertEqual(syntax.decode(None, b"'0'B"), "'0'B")
+		self.assertEqual(syntax.decode(None, b"'010101'B"), "'010101'B")
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"")
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"'0'")
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"'0'b")
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"'0123'B")
+
+class TestIntegerSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.INTEGER
+		self.assertEqual(syntax.encode(None, 0), b'0')
+		self.assertEqual(syntax.encode(None, 1234), b'1234')
+		self.assertEqual(syntax.encode(None, -1234), b'-1234')
+
+	def test_decode(self):
+		syntax = syntaxes.INTEGER
+		self.assertEqual(syntax.decode(None, b'0'), 0)
+		self.assertEqual(syntax.decode(None, b'1234'), 1234)
+		self.assertEqual(syntax.decode(None, b'-1234'), -1234)
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'-0')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'+1')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'0123')
+
+class TestSchemaElementSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		class SchemaElement:
+			def __str__(self):
+				return '( SCHEMA ELEMENT )'
+		syntax = syntaxes.SchemaElementSyntaxDefinition('1.2.3.4')
+		self.assertEqual(syntax.encode(None, SchemaElement()), b'( SCHEMA ELEMENT )')
+
+class TestBooleanSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.Boolean
+		self.assertEqual(syntax.encode(None, True), b'TRUE')
+		self.assertEqual(syntax.encode(None, False), b'FALSE')
+
+	def test_decode(self):
+		syntax = syntaxes.Boolean
+		self.assertEqual(syntax.decode(None, b'TRUE'), True)
+		self.assertEqual(syntax.decode(None, b'FALSE'), False)
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'true')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'')
+
+class TestDNSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.DN
+		self.assertEqual(syntax.encode(None, ldapserver.dn.DN(cn='foobar')), b'cn=foobar')
+
+	def test_decode(self):
+		syntax = syntaxes.DN
+		self.assertEqual(syntax.decode(None, b'cn=foobar'), ldapserver.dn.DN(cn='foobar'))
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'cn=foobar,,,')
+
+class TestNameAndOptionalUIDSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.NameAndOptionalUID
+		self.assertEqual(syntax.encode(None, ldapserver.dn.DN(cn='foobar')), b'cn=foobar')
+		self.assertEqual(syntax.encode(None, ldapserver.dn.DNWithUID(ldapserver.dn.DN(cn='foobar'), "'0101'B")), b"cn=foobar#'0101'B")
+
+	def test_decode(self):
+		syntax = syntaxes.NameAndOptionalUID
+		self.assertEqual(syntax.decode(None, b'cn=foobar'), ldapserver.dn.DN(cn='foobar'))
+		self.assertEqual(syntax.decode(None, b"cn=foobar#'0101'B"), ldapserver.dn.DNWithUID(ldapserver.dn.DN(cn='foobar'), "'0101'B"))
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'cn=foobar,,,')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"cn=foobar,,,#'0101'B")
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b"cn=foobar#'0102'B")
+
+class TestGeneralizedTimeSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.GeneralizedTime
+		self.assertEqual(syntax.encode(None, datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)),
+		                 b'199412161032Z')
+		self.assertEqual(syntax.encode(None, datetime.datetime(1994, 12, 16, 5, 32, tzinfo=datetime.timezone(datetime.timedelta(hours=-5)))),
+		                 b'199412160532-0500')
+
+	def test_decode(self):
+		syntax = syntaxes.GeneralizedTime
+		self.assertEqual(syntax.decode(None, b'199412161032Z'),
+		                 datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc))
+		self.assertEqual(syntax.decode(None, b'199412160532-0500'),
+		                 datetime.datetime(1994, 12, 16, 5, 32, tzinfo=datetime.timezone(datetime.timedelta(hours=-5))))
+
+class TestPostalAddressSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.PostalAddress
+		self.assertEqual(syntax.encode(None, ['1234 Main St.', 'Anytown, CA 12345', 'USA']),
+		                 b'1234 Main St.$Anytown, CA 12345$USA')
+		self.assertEqual(syntax.encode(None, ['$1,000,000 Sweepstakes', 'PO Box 1000000', 'Anytown, CA 12345', 'USA']),
+		                 b'\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA')
+
+	def test_decode(self):
+		syntax = syntaxes.PostalAddress
+		self.assertEqual(syntax.decode(None, b'1234 Main St.$Anytown, CA 12345$USA'),
+		                 ['1234 Main St.', 'Anytown, CA 12345', 'USA'])
+		self.assertEqual(syntax.decode(None, b'\\241,000,000 Sweepstakes$PO Box 1000000$Anytown, CA 12345$USA'),
+		                 ['$1,000,000 Sweepstakes', 'PO Box 1000000', 'Anytown, CA 12345', 'USA'])
+
+class TestSubstringAssertionSyntaxDefinition(unittest.TestCase):
+	def test_decode(self):
+		syntax = syntaxes.SubstringAssertion
+		self.assertEqual(syntax.decode(None, b'*foo*'), (None, ['foo'], None))
+		self.assertEqual(syntax.decode(None, b'*foo*bar*'), (None, ['foo', 'bar'], None))
+		self.assertEqual(syntax.decode(None, b'a*foo*bar*b'), ('a', ['foo', 'bar'], 'b'))
+		self.assertEqual(syntax.decode(None, b'a*b'), ('a', [], 'b'))
+		self.assertEqual(syntax.decode(None, b' a\\2A*\\2Afoo*\\5Cbar*\\2Ab'), (' a*', ['*foo', '\\bar'], '*b'))
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'')
+		with self.assertRaises(ldapserver.exceptions.LDAPInvalidAttributeSyntax):
+			syntax.decode(None, b'foo')
+
+class TestUTCTimeSyntaxDefinition(unittest.TestCase):
+	def test_encode(self):
+		syntax = syntaxes.UTCTime
+		self.assertEqual(syntax.encode(None, datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc)),
+		                 b'9412161032Z')
+		self.assertEqual(syntax.encode(None, datetime.datetime(1994, 12, 16, 5, 32, tzinfo=datetime.timezone(datetime.timedelta(hours=-5)))),
+		                 b'9412160532-0500')
+
+	def test_decode(self):
+		syntax = syntaxes.UTCTime
+		self.assertEqual(syntax.decode(None, b'9412161032Z'),
+		                 datetime.datetime(1994, 12, 16, 10, 32, tzinfo=datetime.timezone.utc))
+		self.assertEqual(syntax.decode(None, b'9412160532-0500'),
+		                 datetime.datetime(1994, 12, 16, 5, 32, tzinfo=datetime.timezone(datetime.timedelta(hours=-5))))
+
+
diff --git a/tests/test_server.py b/tests/test_server.py
index 639b5de88da989d244c63eeef8e3fa4decc24405..a471fcf0759a9bc4b7644fcdb6cfc947b7f35ab6 100644
--- a/tests/test_server.py
+++ b/tests/test_server.py
@@ -139,3 +139,33 @@ class TestLDAPRequestHandler(unittest.TestCase):
 		self.assertEqual(resps[0].protocolOp.resultCode, ldap.LDAPResultCode.success)
 		resps = list(handler.handle_message(ldap.ShallowLDAPMessage.from_ber(b'0\x05\x02\x01\x03B\x00')[0]))
 		self.assertEqual(len(resps), 0)
+
+	def test_search(self):
+		class MockObject:
+			def __init__(_self, search_result=None):
+				_self.search_result = search_result
+
+			def search(_self, base_obj, scope, filter_obj, attributes, types_only):
+				self.assertEqual(base_obj, 'cn=Test,dc=example,dc=com')
+				self.assertEqual(scope, ldap.SearchScope.singleLevel)
+				self.assertEqual(ldap.Filter.to_ber(filter_obj), ldap.Filter.to_ber(ldap.FilterPresent('foobar')))
+				return _self.search_result
+
+		class RequestHandler(LDAPRequestHandler):
+			def handle(self):
+				pass
+
+			def do_search(_self, base_obj, scope, filter_obj):
+				self.assertEqual(base_obj, 'cn=Test,dc=example,dc=com')
+				self.assertEqual(scope, ldap.SearchScope.singleLevel)
+				self.assertEqual(ldap.Filter.to_ber(filter_obj), ldap.Filter.to_ber(ldap.FilterPresent('foobar')))
+				yield MockObject(ldap.SearchResultEntry('cn=Test1,dc=example,dc=com'))
+				yield MockObject(None)
+				yield MockObject(ldap.SearchResultEntry('cn=Test2,dc=example,dc=com'))
+
+		handler = RequestHandler(None, None, None)
+		resps = list(handler.handle_search(ldap.SearchRequest('cn=Test,dc=example,dc=com', ldap.SearchScope.singleLevel, filter=ldap.FilterPresent('foobar')), []))
+		self.assertEqual(len(resps), 3)
+		self.assertEqual(ldap.ProtocolOp.to_ber(resps[0]), ldap.ProtocolOp.to_ber(ldap.SearchResultEntry('cn=Test1,dc=example,dc=com')))
+		self.assertEqual(ldap.ProtocolOp.to_ber(resps[1]), ldap.ProtocolOp.to_ber(ldap.SearchResultEntry('cn=Test2,dc=example,dc=com')))
+		self.assertEqual(ldap.ProtocolOp.to_ber(resps[2]), ldap.ProtocolOp.to_ber(ldap.SearchResultDone()))
diff --git a/tests/test_stringprep.py b/tests/test_stringprep.py
new file mode 100644
index 0000000000000000000000000000000000000000..43b2612ed87d51edad5a56abe970a778e9a15222
--- /dev/null
+++ b/tests/test_stringprep.py
@@ -0,0 +1,21 @@
+import unittest
+import enum
+
+from ldapserver.rfc4518_stringprep import prepare, MatchingType, SubstringType
+
+class TestStringprep(unittest.TestCase):
+	def test_map(self):
+		self.assertEqual(prepare('foo\n\rbar', MatchingType.EXACT_STRING, SubstringType.NONE), ' foo  bar ')
+		self.assertEqual(prepare('foo\u200Bbar', MatchingType.EXACT_STRING, SubstringType.NONE), ' foobar ')
+
+	# TODO: systematic test cases
+
+	def test_examples(self):
+		# Examples from RFC4518 for "Insignificant Character Handling"
+		self.assertEqual(prepare('foo bar  ', MatchingType.EXACT_STRING, SubstringType.NONE), ' foo  bar ')
+		self.assertEqual(prepare('foo bar  ', MatchingType.EXACT_STRING, SubstringType.INITIAL), ' foo  bar ')
+		self.assertEqual(prepare('foo bar  ', MatchingType.EXACT_STRING, SubstringType.ANY), 'foo  bar ')
+		self.assertEqual(prepare('  123  456  ', MatchingType.NUMERIC_STRING), '123456')
+		self.assertEqual(prepare('   ', MatchingType.NUMERIC_STRING), '')
+		self.assertEqual(prepare(' -123  456 -', MatchingType.TELEPHONE_NUMBER), '123456')
+		self.assertEqual(prepare('---', MatchingType.TELEPHONE_NUMBER), '')