diff --git a/LDAPv3.cabal b/LDAPv3.cabal
new file mode 100644
--- /dev/null
+++ b/LDAPv3.cabal
@@ -0,0 +1,90 @@
+cabal-version:       2.2
+name:                LDAPv3
+version:             0.0.0.0
+
+synopsis:            Lightweight Directory Access Protocol (LDAP) version 3
+license:             GPL-2.0-or-later
+license-file:        LICENSE
+author:              Herbert Valerio Riedel
+copyright:           © 2018-2019 Herbert Valerio Riedel
+maintainer:          hvr@gnu.org
+bug-reports:         https://github.com/hvr/LDAPv3/issues
+category:            Network
+description:
+  This library provides a pure Haskell implementation of the /Lightweight Directory Access Protocol (LDAP)/ version 3 as specified in <https://tools.ietf.org/html/rfc4511 RFC4511>.
+  .
+  Serializing and deserializing to and from the wire <https://en.wikipedia.org/wiki/ASN.1 ASN.1> encoding for the purpose of implementing network clients and servers is supported via 'Binary' instances (see <//hackage.haskell.org/package/binary 'binary' package>).
+
+source-repository head
+  type:     git
+  location: https://github.com/hvr/LDAPv3.git
+
+common defaults
+  default-language: Haskell2010
+  other-extensions:
+    BangPatterns
+    ConstraintKinds
+    DataKinds
+    DeriveFunctor
+    DeriveGeneric
+    FlexibleContexts
+    FlexibleInstances
+    GeneralizedNewtypeDeriving
+    KindSignatures
+    LambdaCase
+    MultiParamTypeClasses
+    RecordWildCards
+    ScopedTypeVariables
+    TypeFamilies
+    TypeOperators
+    UndecidableInstances
+
+  build-depends:
+    , base          >= 4.8 && < 4.14
+    , binary       ^>= 0.8.3
+    , bytestring   ^>= 0.10.4
+    , text-short   ^>= 0.1.3
+    , containers   ^>= 0.5.5 || ^>= 0.6.0
+    , deepseq      ^>= 1.4.0
+    , int-cast     ^>= 0.2.0
+    , newtype      ^>= 0.2.2
+
+  if !impl(ghc >= 8.0)
+    build-depends:
+      , semigroups ^>= 0.19.1
+
+  ghc-options: -Wall
+
+-------------------------------------------------------------------------------
+
+library
+  import: defaults
+
+  hs-source-dirs: src
+  exposed-modules:
+      LDAPv3
+  other-modules:
+      Common
+      Data.Int.Subtypes
+      Data.ASN1
+      Data.ASN1.Prim
+      LDAPv3.ResultCode
+
+-------------------------------------------------------------------------------
+
+test-suite LDAPv3
+  import: defaults
+
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  main-is: main.hs
+  other-modules: Arbitrary
+
+  build-depends:
+    , LDAPv3
+
+    , tasty            ^>= 1.2.3
+    , tasty-quickcheck ^>= 0.10.1
+    , tasty-hunit      ^>= 0.10.0
+    , base-encoding    ^>= 0.1.0
+    , quickcheck-instances ^>= 0.3.22
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,341 @@
+
+             GNU GENERAL PUBLIC LICENSE
+                Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                     Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+             GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                     NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+              END OF TERMS AND CONDITIONS
+
+     How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+
diff --git a/src/Common.hs b/src/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Common.hs
@@ -0,0 +1,29 @@
+module Common (module Common, module X) where
+
+import           Control.Applicative as X
+import           Control.DeepSeq     as X (NFData (rnf))
+import           Control.Exception   as X (ArithException (Overflow, Underflow), throw)
+import           Control.Monad       as X
+import           Control.Newtype     as X (Newtype (..))
+import           Data.Bits           as X
+import           Data.ByteString     as X (ByteString)
+import           Data.Int            as X
+import           Data.IntCast        as X
+import           Data.List.NonEmpty  as X (NonEmpty (..))
+import           Data.Maybe          as X
+import           Data.Proxy          as X (Proxy (Proxy))
+import           Data.Semigroup      as X
+import           Data.Text.Short     as X (ShortText)
+import           Data.Word           as X
+import           GHC.Generics        as X (Generic)
+import           GHC.TypeLits        as X
+
+{-# INLINE rwhnf #-}
+rwhnf :: a -> ()
+rwhnf x = seq x ()
+
+{-# INLINE inside #-}
+inside :: Ord a => a -> (a, a) -> Bool
+x `inside` (lb,ub)
+  | lb > ub = error "inside: unsatifiable range"
+  | otherwise = lb <= x && x <= ub
diff --git a/src/Data/ASN1.hs b/src/Data/ASN1.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ASN1.hs
@@ -0,0 +1,671 @@
+-- Copyright (c) 2018-2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE ConstraintKinds            #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE DeriveFunctor              #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE FlexibleInstances          #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE MultiParamTypeClasses      #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE TypeOperators              #-}
+
+module Data.ASN1
+    ( ASN1(..)
+    , ASN1Decode
+    , ASN1Encode
+
+    , ENUMERATED(..), Enumerated(..)
+    , IMPLICIT(..), implicit
+    , EXPLICIT(..), explicit
+
+    , OCTET_STRING
+    , NULL
+    , BOOLEAN
+    , BOOLEAN_DEFAULT_FALSE(..)
+    , OPTIONAL
+
+    , SET(..)
+    , SET1(..)
+
+    , toBinaryPut
+    , toBinaryGet
+
+    , retag, wraptag
+
+    , with'SEQUENCE
+    , enc'SEQUENCE
+    , enc'SEQUENCE_COMPS
+
+    , with'CHOICE
+
+    , dec'BoundedEnum
+    , enc'BoundedEnum
+
+    , dec'NULL
+    , enc'NULL
+    ) where
+
+import           Common
+import           Data.ASN1.Prim
+import           Data.Int.Subtypes
+
+import           Data.Binary           as Bin
+import           Data.Binary.Get       as Bin
+import           Data.Binary.Put       as Bin
+import           Data.Bool             (bool)
+import qualified Data.ByteString       as BS
+import qualified Data.ByteString.Short as SBS
+import           Data.Set              (Set)
+import qualified Data.Set              as Set
+import           Data.String           (IsString)
+import qualified Data.Text.Short       as TS
+
+----------------------------------------------------------------------------
+
+class Enumerated x where
+  toEnumerated :: Int64 -> Maybe x
+  fromEnumerated :: x -> Int64
+
+instance Enumerated Int64 where
+  toEnumerated = Just
+  fromEnumerated = id
+
+instance Enumerated Int where
+  toEnumerated = intCastMaybe
+  fromEnumerated = fromIntegral
+
+----------------------------------------------------------------------------
+
+data ASN1Res x = Consumed ({- leftover -} Maybe TL) x
+               | Unexpected {- leftover -} TL
+               | UnexpectedEOF
+               deriving (Show,Functor)
+
+newtype ASN1Encode a = ASN1Encode (Maybe Tag -> PutM a)
+
+empty'ASN1Encode :: ASN1Encode Word64
+empty'ASN1Encode = ASN1Encode $ \case
+  Just _  -> error "empty'ASN1Encode: called with tag-override"
+  Nothing -> pure 0
+
+toBinaryPut :: ASN1Encode a -> PutM a
+toBinaryPut (ASN1Encode body) = body Nothing
+
+enc'SEQUENCE_COMPS :: [ASN1Encode Word64] -> ASN1Encode Word64
+enc'SEQUENCE_COMPS [] = empty'ASN1Encode
+enc'SEQUENCE_COMPS xs0 = ASN1Encode $ \case
+    Just _  -> error "enc'SEQUENCE_COMPS: called with tag-override"
+    Nothing -> go xs0 0
+  where
+    go [] sz = pure sz
+    go (ASN1Encode x:xs) sz = do
+      n1 <- x Nothing
+      go xs (sz+n1)
+
+enc'SEQUENCE :: [ASN1Encode Word64] -> ASN1Encode Word64
+enc'SEQUENCE = wraptag (Universal 16) . enc'SEQUENCE_COMPS
+
+enc'SET :: [ASN1Encode Word64] -> ASN1Encode Word64
+enc'SET = retag (Universal 17) . enc'SEQUENCE
+
+data ASN1Decode x = ASN1Decode { asn1dTags    :: !(Set Tag)
+                               , asn1dAny     :: !Bool
+                               , asn1dContent :: Maybe TL {- Nothing == EOF -} -> Get (ASN1Res x)
+                               }
+
+
+
+
+asn1DecodeSingleton :: Tag -> (TL -> Get x) -> ASN1Decode x
+asn1DecodeSingleton t c = mempty { asn1dTags    = Set.singleton t
+                                 , asn1dContent = \case
+                                     Just tl@(t',_,_) | t /= t' -> pure (Unexpected tl)
+                                                      | otherwise -> Consumed Nothing <$> c tl
+                                     Nothing -> pure UnexpectedEOF
+                                 }
+
+
+asn1DecodeSingleton' :: Tag -> (TL -> Get (ASN1Res x)) -> ASN1Decode x
+asn1DecodeSingleton' t c = mempty { asn1dTags    = Set.singleton t
+                                  , asn1dContent = \case
+                                      Just tl@(t',_,_) | t /= t' -> pure (Unexpected tl)
+                                                       | otherwise -> c tl
+                                      Nothing -> pure UnexpectedEOF
+                                  }
+
+
+asn1decodeIsSingleton :: ASN1Decode x -> Maybe Tag
+asn1decodeIsSingleton (ASN1Decode {..})
+  | asn1dAny                     = Nothing
+  | [t1] <- Set.toList asn1dTags = Just t1
+  | otherwise                    = Nothing
+
+with'OPTIONAL :: ASN1Decode x -> ASN1Decode (Maybe x)
+with'OPTIONAL x = x { asn1dAny = True
+                    , asn1dContent = \case
+                        Nothing -> pure $ Consumed Nothing Nothing
+                        Just tl -> g <$> asn1dContent x (Just tl)
+                    }
+  where
+    g (Consumed mleftover v) = Consumed mleftover (Just v)
+    g (Unexpected leftover)  = Consumed (Just leftover) Nothing
+    g UnexpectedEOF          = Consumed Nothing Nothing
+
+-- | Left-biased "CHOICE" join (TODO: verify specific-match-first semantics are sane in presence of ANYs)
+instance Semigroup (ASN1Decode x) where
+  x <> y
+    | asn1decodeIsEmpty x = y
+    | asn1decodeIsEmpty y = x
+    | otherwise = ASN1Decode
+                  { asn1dTags = asn1dTags x <> asn1dTags y
+                  , asn1dAny  = asn1dAny x || asn1dAny y
+                  , asn1dContent = \case
+                          tl@(Just (t,_,_)) -> case () of
+                            _ | Set.member t (asn1dTags x) -> asn1dContent x tl
+                              | Set.member t (asn1dTags y) -> asn1dContent y tl
+                              | asn1dAny x -> asn1dContent x tl
+                              | asn1dAny y -> asn1dContent y tl
+                              | otherwise  -> fail "asn1dContent called with unsupported Tag" -- internal error
+                          Nothing -> case () of
+                            _ | asn1dAny x -> asn1dContent x Nothing
+                              | asn1dAny y -> asn1dContent y Nothing
+                              | otherwise  -> pure UnexpectedEOF
+                  }
+
+-- | Test whether 'mempty'
+asn1decodeIsEmpty :: ASN1Decode x -> Bool
+asn1decodeIsEmpty ASN1Decode{..} = not asn1dAny && Set.null asn1dTags
+
+instance Monoid (ASN1Decode x) where
+  mempty = ASN1Decode mempty False (pure . maybe UnexpectedEOF Unexpected)
+  mappend = (<>)
+  -- TODO: optimized mconcat
+
+instance Functor ASN1Decode where
+  fmap f dec = dec { asn1dContent = \tl -> fmap f <$> asn1dContent dec tl }
+
+instance Applicative ASN1Decode where
+  pure x = mempty { asn1dAny = True
+                  , asn1dContent = \tl -> pure (Consumed tl x)
+                  }
+  (<*>) = ap
+
+instance Monad ASN1Decode where
+  return = pure -- redundant for base >= 4.8
+
+  mx >>= k = ASN1Decode { asn1dAny = asn1dAny mx
+                        , asn1dTags = asn1dTags mx
+                        , asn1dContent = \mtl -> do
+                            x0 <- getASN1Decode mx mtl
+                            case x0 of
+                              Consumed (Just tl') x -> do
+                                getASN1Decode (k x) (Just tl')
+                              Consumed Nothing x -> do
+                                mtl' <- getTagLength BER
+                                getASN1Decode (k x) mtl'
+                              Unexpected (t,_,_) ->
+                                fail ("ASN1Decode: Unexpected " ++ show t)
+                              UnexpectedEOF ->
+                                fail ("ASN1Decode: UnexpectedEOF")
+                        }
+
+-- instance MonadFail ASN1Decode where
+--   fail = asn1fail
+
+asn1fail :: String -> ASN1Decode a
+asn1fail s = mempty { asn1dAny = True
+                    , asn1dContent = \_ -> fail s
+                    }
+
+toBinaryGet :: ASN1Decode x -> Get x
+toBinaryGet dec
+  = getTagLength BER >>= getASN1Decode dec >>= \case
+      Unexpected tl -> fail ("ASN1Decode: unexpected " ++ show tl)
+      UnexpectedEOF -> fail "ASN1Decode: premature end of stream"
+      Consumed (Just tl) _ -> fail ("ASN1Decode: leftover " ++ show tl)
+      Consumed Nothing x -> pure x
+
+getASN1Decode :: ASN1Decode x -> Maybe TL -> Get (ASN1Res x)
+getASN1Decode (ASN1Decode{..}) Nothing
+  | asn1dAny  = asn1dContent Nothing
+  | otherwise = pure UnexpectedEOF
+getASN1Decode (ASN1Decode{..}) (Just tl@(t,_,_))
+  | asn1dAny || Set.member t asn1dTags  = asn1dContent (Just tl)
+  | otherwise                           = pure (Unexpected tl)
+
+----------------------------------------------------------------------------
+-- simple ASN.1 EDSL
+
+-- bind-like transform
+transformVia :: ASN1Decode x -> (x -> Either String y) -> ASN1Decode y
+transformVia old f
+  = old { asn1dContent = \mtl -> do
+            asn1dContent old mtl >>= \case
+              Consumed lo x -> case f x of
+                                 Left e  -> fail e
+                                 Right y -> pure (Consumed lo y)
+              Unexpected u  -> pure (Unexpected u)
+              UnexpectedEOF -> pure UnexpectedEOF
+        }
+
+explicit :: Tag -> ASN1Decode x -> ASN1Decode x
+explicit t body = with'Constructed (show t ++ " EXPLICIT") t body
+
+implicit :: Tag -> ASN1Decode x -> ASN1Decode x
+implicit newtag old
+  | Just oldtag <- asn1decodeIsSingleton old
+  = mempty { asn1dTags    = Set.singleton newtag
+           , asn1dContent = \case
+               Just tl@(curtag,_,_) | newtag /= curtag -> pure (Unexpected tl)
+               Just (_,pc,sz) -> asn1dContent old (Just (oldtag,pc,sz))
+               Nothing        -> asn1dContent old Nothing
+           }
+  | otherwise = error "implicit applied to non-singleton ASN1Decode"
+
+with'CHOICE :: [ASN1Decode x] -> ASN1Decode x
+with'CHOICE = mconcat
+
+with'Constructed :: forall x . String -> Tag -> ASN1Decode x -> ASN1Decode x
+with'Constructed l tag body = asn1DecodeSingleton' tag go
+  where
+    go :: TL -> Get (ASN1Res x)
+    go (_,Primitive,_) = fail (l ++ " with primitive encoding")
+    go (_,Constructed,Nothing) = fail (l ++ " with indef length not supported yet")
+    go (_,Constructed,Just sz) = isolate64 sz $ do
+          tl' <- getTagLength BER
+          getASN1Decode body tl'
+
+with'SEQUENCE :: forall x . ASN1Decode x -> ASN1Decode x
+with'SEQUENCE = with'Constructed "SEQUENCE" (Universal 16)
+
+with'SEQUENCE_OF :: forall x . ASN1Decode x -> ASN1Decode [x]
+with'SEQUENCE_OF body = asn1DecodeSingleton' (Universal 16) go
+  where
+    go :: TL -> Get (ASN1Res [x])
+    go (_,Primitive,_)         = fail "SEQUENCE OF with primitive encoding"
+    go (_,Constructed,Nothing) = fail "indef SEQUENCE OF not implemented yet"
+    go (_,Constructed,Just sz) = isolate64 sz $ do
+          -- NB: Get Monad
+          let loop :: [x] -> Maybe TL -> Get [x]
+              loop acc tl0 = do
+                tl' <- case tl0 of
+                         Just _  -> pure tl0
+                         Nothing -> getTagLength BER
+                case tl' of
+                  Nothing -> pure (reverse acc)
+                  Just _  -> do
+                    tmp <- getASN1Decode body tl'
+                    case tmp of
+                      Consumed tl'' v -> loop (v:acc) tl''
+                      UnexpectedEOF   -> fail "with'SEQUENCE_OF: unexpected EOF"
+                      Unexpected t    -> fail ("with'SEQUENCE_OF: unexpected " ++ show t)
+
+          Consumed Nothing <$> loop [] Nothing
+
+
+with'SET_OF :: forall x . ASN1Decode x -> ASN1Decode [x]
+with'SET_OF body = asn1DecodeSingleton' (Universal 17) go
+  where
+    go :: TL -> Get (ASN1Res [x])
+    go (_,Primitive,_)         = fail "SET OF with primitive encoding"
+    go (_,Constructed,Nothing) = fail "indef SET OF not implemented yet"
+    go (_,Constructed,Just sz) = isolate64 sz $ do
+          -- NB: Get Monad
+          let loop :: [x] -> Maybe TL -> Get [x]
+              loop acc tl0 = do
+                tl' <- case tl0 of
+                         Just _  -> pure tl0
+                         Nothing -> getTagLength BER
+                case tl' of
+                  Nothing -> pure (reverse acc)
+                  Just _  -> do
+                    tmp <- getASN1Decode body tl'
+                    case tmp of
+                      Consumed tl'' v -> loop (v:acc) tl''
+                      UnexpectedEOF   -> fail "with'SET_OF: unexpected EOF"
+                      Unexpected t    -> fail ("with'SET_OF: unexpected " ++ show t)
+
+          Consumed Nothing <$> loop [] Nothing
+
+
+
+dec'BOOLEAN :: ASN1Decode Bool
+dec'BOOLEAN = asn1DecodeSingleton (Universal 1) $ asPrimitive go
+  where
+    go 1 = do
+      x <- getWord8
+      case x of
+        0x00 -> pure False
+        0xff -> pure True
+        _    -> fail "BOOLEAN must be encoded as either 0x00 or 0xFF" -- enforce DER/DER rules here
+    go _ = fail "BOOLEAN with content-length not equal 1"
+
+enc'BOOLEAN :: Bool -> ASN1Encode Word64
+enc'BOOLEAN v = ASN1Encode $ \mt -> do
+  _ <- putTagLength (Universal 1 `fromMaybe` mt, Primitive, Just 1)
+  putWord8 (if v then 0xff else 0x00)
+  pure 3
+
+{- TODO
+getPrim'Boolean :: EncodingRule -> Word64 -> Get Bool
+getPrim'Boolean r sz
+  | sz /= 1 = fail "boolean content shall be a single octet"
+  | otherwise = do
+      x <- getWord8
+      case (r,x) of
+        (_,0x00)   -> pure False
+        (BER,_)    -> pure True
+        (CER,0xff) -> pure True
+        (CER,_)    -> fail "all bits shall be set in boolean TRUE encoding for CER"
+        (DER,0xff) -> pure True
+        (DER,_)    -> fail "all bits shall be set in boolean TRUE encoding for DER"
+-}
+
+dec'INTEGER :: ASN1Decode Integer
+dec'INTEGER = asn1DecodeSingleton (Universal 2) $ asPrimitive getVarInteger
+
+enc'INTEGER :: Integer -> ASN1Encode Word64
+enc'INTEGER i = wrap'DEFINITE (Universal 2) Primitive (putVarInteger i)
+
+dec'UInt :: forall lb ub t . (UIntBounds lb ub t, Num t) => ASN1Decode (UInt lb ub t)
+dec'UInt = do
+  i <- dec'INTEGER -- TODO: size-hint
+  case uintFromInteger (toInteger i) of
+    Left Underflow -> asn1fail "INTEGER below lower bound"
+    Left Overflow  -> asn1fail "INTEGER above upper bound"
+    Left _         -> asn1fail "INTEGER"
+    Right v        -> pure v
+
+enc'UInt :: forall lb ub t . (UIntBounds lb ub t, Num t, Integral t) => UInt lb ub t -> ASN1Encode Word64
+enc'UInt = enc'INTEGER . toInteger . fromUInt
+
+dec'Int64 :: ASN1Decode Int64
+dec'Int64 = asn1DecodeSingleton (Universal 2) $ asPrimitive getVarInt64
+
+enc'Int64 :: Int64 -> ASN1Encode Word64
+enc'Int64 i = wrap'DEFINITE (Universal 2) Primitive (putVarInt64 i)
+
+dec'ENUMERATED :: Enumerated enum => ASN1Decode enum
+dec'ENUMERATED = asn1DecodeSingleton (Universal 10) $ asPrimitive $ \sz -> do
+    i <- go sz
+    maybe (fail "invalid ENUMERATED value") pure (toEnumerated i)
+  where
+    go 0 = fail "ENUMERATED with empty content"
+    go sz
+      | sz <= 8   = getVarInt64 sz
+      | otherwise = fail "invalid ENUMERATED value"
+
+enc'ENUMERATED :: Enumerated enum => enum -> ASN1Encode Word64
+enc'ENUMERATED = retag (Universal 10) . enc'Int64 . fromEnumerated
+
+-- | Only for non-sparse 'Enum's
+dec'BoundedEnum :: forall enum . (Bounded enum, Enum enum) => ASN1Decode enum
+dec'BoundedEnum = do
+    i <- dec'ENUMERATED
+    unless (i `inside` (lb,ub)) $ asn1fail "invalid ENUMERATED value"
+    pure (toEnum i)
+  where
+    lb = fromEnum (minBound :: enum)
+    ub = fromEnum (maxBound :: enum)
+
+enc'BoundedEnum :: Enum enum => enum -> ASN1Encode Word64
+enc'BoundedEnum v = enc'ENUMERATED (fromIntegral (fromEnum v) :: Int64)
+
+dec'NULL :: ASN1Decode ()
+dec'NULL = asn1DecodeSingleton (Universal 5) $ asPrimitive go
+  where
+    go 0 = pure ()
+    go _ = fail "NULL with content-length not equal 0"
+
+enc'NULL :: ASN1Encode Word64
+enc'NULL = ASN1Encode $ \mt -> putTagLength (Universal 5 `fromMaybe` mt, Primitive, Just 0)
+
+
+dec'OCTETSTRING :: ASN1Decode ByteString
+dec'OCTETSTRING = asn1DecodeSingleton (Universal 4) $ asPrimitive go
+  where
+    go sz
+      | Just sz' <- intCastMaybe sz = Bin.getByteString sz'
+      | otherwise = fail "OCTET STRING too large for this implementation"
+
+enc'OCTETSTRING :: ByteString -> ASN1Encode Word64
+enc'OCTETSTRING bs = ASN1Encode $ \mt -> do
+  let cl = fromIntegral (BS.length bs)
+  hl <- putTagLength (Universal 4 `fromMaybe` mt, Primitive, Just cl)
+  Bin.putByteString bs
+  pure (hl + cl)
+
+wrap'DEFINITE :: Tag -> TagPC -> PutM Word64 -> ASN1Encode Word64
+wrap'DEFINITE t0 pc body = ASN1Encode $ \mt -> do
+  let (cl, lbs) = Bin.runPutM body
+  hl <- putTagLength (fromMaybe t0 mt, pc, Just cl)
+  Bin.putLazyByteString lbs
+  pure (hl+cl)
+
+
+retag :: Tag -> ASN1Encode a -> ASN1Encode a
+retag newtag (ASN1Encode old) = ASN1Encode (\mt -> old (mt <|> Just newtag))
+
+wraptag :: Tag -> ASN1Encode Word64 -> ASN1Encode Word64
+wraptag newtag (ASN1Encode old) = wrap'DEFINITE newtag Constructed (old Nothing)
+
+----------------------------------------------------------------------------
+
+-- | ASN.1 @IMPLICIT@ Annotation
+newtype IMPLICIT (tag :: TagK) x = IMPLICIT x
+  deriving (Generic,NFData,IsString,Num,Show,Eq,Ord,Enum)
+
+instance Newtype (IMPLICIT tag x) x
+
+-- | ASN.1 @EXPLICIT@ Annotation
+newtype EXPLICIT (tag :: TagK) x = EXPLICIT x
+  deriving (Generic,NFData,IsString,Num,Show,Eq,Ord,Enum)
+
+instance Newtype (EXPLICIT tag x) x
+
+-- | ASN.1 @ENUMERATED@ Annotation
+newtype ENUMERATED x = ENUMERATED x
+  deriving (Generic,NFData,Num,Show,Eq,Ord,Enum)
+
+instance Newtype (ENUMERATED x) x
+
+----------------------------------------------------------------------------
+
+class ASN1 t where
+  asn1decode :: ASN1Decode t
+  asn1decode = with'Constructed "SEQUENCE" (asn1defTag (Proxy :: Proxy t)) asn1decodeCompOf
+
+  asn1decodeCompOf :: ASN1Decode t
+  asn1decodeCompOf = asn1fail "asn1decodeCompOf not implemented for type"
+
+  asn1encode :: t -> ASN1Encode Word64
+  asn1encode = wraptag (asn1defTag (Proxy :: Proxy t)) . asn1encodeCompOf
+
+  -- constructed contents
+  asn1encodeCompOf :: t -> ASN1Encode Word64
+  asn1encodeCompOf = error "asn1encode(CompOf) not implemented for type"
+
+  -- default-tag
+  asn1defTag :: Proxy t -> Tag
+  asn1defTag _ = Universal 16
+
+  {-# MINIMAL (asn1decode | asn1decodeCompOf), (asn1encode | asn1encodeCompOf) #-}
+
+instance (ASN1 t1, ASN1 t2) => ASN1 (t1,t2) where
+  asn1encodeCompOf (v1,v2) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2]
+  asn1decodeCompOf = (,) <$> asn1decode <*> asn1decode
+
+instance (ASN1 t1, ASN1 t2, ASN1 t3) => ASN1 (t1,t2,t3) where
+  asn1encodeCompOf (v1,v2,v3) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2, asn1encode v3]
+  asn1decodeCompOf = (,,) <$> asn1decode <*> asn1decode <*> asn1decode
+
+instance (ASN1 t1, ASN1 t2, ASN1 t3, ASN1 t4) => ASN1 (t1,t2,t3,t4) where
+  asn1encodeCompOf (v1,v2,v3,v4) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2, asn1encode v3, asn1encode v4]
+  asn1decodeCompOf = (,,,) <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+
+instance (ASN1 t1, ASN1 t2, ASN1 t3, ASN1 t4, ASN1 t5) => ASN1 (t1,t2,t3,t4,t5) where
+  asn1encodeCompOf (v1,v2,v3,v4,v5) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2, asn1encode v3, asn1encode v4, asn1encode v5]
+  asn1decodeCompOf = (,,,,) <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+
+instance (ASN1 t1, ASN1 t2, ASN1 t3, ASN1 t4, ASN1 t5, ASN1 t6) => ASN1 (t1,t2,t3,t4,t5,t6) where
+  asn1encodeCompOf (v1,v2,v3,v4,v5,v6) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2, asn1encode v3, asn1encode v4, asn1encode v5, asn1encode v6]
+  asn1decodeCompOf = (,,,,,) <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+
+instance (ASN1 t1, ASN1 t2, ASN1 t3, ASN1 t4, ASN1 t5, ASN1 t6, ASN1 t7) => ASN1 (t1,t2,t3,t4,t5,t6,t7) where
+  asn1encodeCompOf (v1,v2,v3,v4,v5,v6,v7) = enc'SEQUENCE_COMPS [asn1encode v1, asn1encode v2, asn1encode v3, asn1encode v4, asn1encode v5, asn1encode v6, asn1encode v7]
+  asn1decodeCompOf = (,,,,,,) <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+
+-- | ASN.1 @OCTET STRING@ type
+type OCTET_STRING = ByteString
+
+instance ASN1 ByteString where
+  asn1defTag _ = Universal 4
+  asn1decode = dec'OCTETSTRING
+  asn1encode = enc'OCTETSTRING
+
+instance ASN1 SBS.ShortByteString where
+  asn1defTag _ = Universal 4
+  asn1decode = SBS.toShort <$> dec'OCTETSTRING
+  asn1encode = enc'OCTETSTRING . SBS.fromShort -- TODO: optimize
+
+instance ASN1 ShortText where
+  asn1defTag _ = Universal 4
+  asn1decode = do
+    bs <- dec'OCTETSTRING
+    maybe (asn1fail "OCTECT STRING contained invalid UTF-8") pure (TS.fromByteString bs)
+  asn1encode = asn1encode . TS.toShortByteString
+
+type BOOLEAN = Bool
+
+instance ASN1 Bool where
+  asn1defTag _ = Universal 1
+  asn1decode = dec'BOOLEAN
+  asn1encode = enc'BOOLEAN
+
+type OPTIONAL x = Maybe x
+
+instance ASN1 t => ASN1 (Maybe t) where
+  asn1defTag _ = asn1defTag (Proxy :: Proxy t)
+  asn1decode = with'OPTIONAL asn1decode
+
+  asn1encode Nothing  = empty'ASN1Encode
+  asn1encode (Just v) = asn1encode v
+
+instance Enumerated t => ASN1 (ENUMERATED t) where
+  asn1defTag _ = Universal 10
+  asn1decode = ENUMERATED <$> dec'ENUMERATED
+  asn1encode (ENUMERATED v) = enc'ENUMERATED v
+
+instance ASN1 t => ASN1 [t] where
+  asn1decode = with'SEQUENCE_OF asn1decode
+  asn1encode = enc'SEQUENCE . map asn1encode
+
+-- | @SEQUENCE SIZE (1..MAX) OF@
+instance ASN1 t => ASN1 (NonEmpty t) where
+  asn1decode = asn1decode >>= \case
+                 []   -> asn1fail "SEQUENCE must be non-empty"
+                 x:xs -> pure (x :| xs)
+
+  asn1encode (x :| xs) = asn1encode (x:xs)
+
+-- | ASN.1 @SET SIZE (1..MAX) OF@ type
+newtype SET1 x = SET1 (NonEmpty x)
+  deriving (Generic,NFData,Show,Eq,Ord)
+
+instance Newtype (SET1 x) (NonEmpty x)
+
+instance ASN1 t => ASN1 (SET1 t) where
+  asn1defTag _ = Universal 17
+  asn1decode = asn1decode >>= \case
+                 SET [] -> asn1fail "SET must be non-empty"
+                 SET (x:xs) -> pure (SET1 (x :| xs))
+
+  asn1encode (SET1 (x :| xs)) = asn1encode (SET (x:xs))
+
+-- | ASN.1 @SET OF@ type
+newtype SET x = SET [x]
+  deriving (Generic,NFData,Show,Eq,Ord)
+
+instance Newtype (SET x) [x]
+
+instance ASN1 t => ASN1 (SET t) where
+  asn1defTag _ = Universal 17
+  asn1decode = SET <$> with'SET_OF asn1decode
+  asn1encode (SET vs) = enc'SET (map asn1encode vs)
+
+instance ASN1 Integer where
+  asn1defTag _ = Universal 2
+  asn1decode = dec'INTEGER
+  asn1encode = enc'INTEGER
+
+instance ASN1 Int64 where
+  asn1defTag _ = Universal 2
+  asn1decode = dec'Int64
+  asn1encode = enc'Int64
+
+instance (UIntBounds lb ub t, Integral t) => ASN1 (UInt lb ub t) where
+  asn1defTag _ = Universal 2
+  asn1decode = dec'UInt
+  asn1encode = enc'UInt
+
+instance forall tag t . (KnownTag tag, ASN1 t) => ASN1 (IMPLICIT tag t) where
+  asn1defTag _ = tagVal (Proxy :: Proxy tag)
+  asn1decode = IMPLICIT <$> implicit (tagVal (Proxy :: Proxy tag)) asn1decode
+  asn1encode (IMPLICIT v) = retag (tagVal (Proxy :: Proxy tag)) (asn1encode v)
+
+instance forall tag t . (KnownTag tag, ASN1 t) => ASN1 (EXPLICIT tag t) where
+  asn1defTag _ = tagVal (Proxy :: Proxy tag)
+  asn1decode = EXPLICIT <$> explicit (tagVal (Proxy :: Proxy tag)) asn1decode
+  asn1encode (EXPLICIT v) = wraptag (tagVal (Proxy :: Proxy tag)) (asn1encode v)
+
+-- | ASN.1 @NULL@ type
+type NULL = ()
+
+-- | denotes @NULL@
+instance ASN1 () where
+  asn1defTag _ = Universal 5
+  asn1decode = dec'NULL
+  asn1encode () = enc'NULL
+
+-- | This represents a @BOOLEAN DEFAULT FALSE@ that is only ever serialized as 'True' (hence why its only inhabitant is a /true/ value)
+--
+-- This must be 'Maybe'-wrapped to make any sense; the table below shows the mapping between 'Bool' values and this construct.
+--
+-- +---------+-----------------------------------+
+-- | 'Bool'  | @'Maybe' 'BOOLEAN_DEFAULT_FALSE'@ |
+-- +=========+===================================+
+-- | 'False' | 'Nothing'                         |
+-- +---------+-----------------------------------+
+-- | 'True'  | @'Just' 'BOOL_TRUE'@              |
+-- +---------+-----------------------------------+
+--
+data BOOLEAN_DEFAULT_FALSE = BOOL_TRUE
+  deriving (Generic,Eq,Ord,Show)
+
+instance NFData BOOLEAN_DEFAULT_FALSE where
+  rnf BOOL_TRUE = ()
+
+instance ASN1 BOOLEAN_DEFAULT_FALSE where
+  asn1defTag _ = Universal 1 -- not used
+  asn1decode = dec'BOOLEAN `transformVia`
+               bool (Left "FALSE encountered despite 'BOOLEAN DEFAULT FALSE'") (Right BOOL_TRUE)
+  asn1encode BOOL_TRUE = asn1encode True
diff --git a/src/Data/ASN1/Prim.hs b/src/Data/ASN1/Prim.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/ASN1/Prim.hs
@@ -0,0 +1,336 @@
+-- Copyright (c) 2018-2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE BangPatterns               #-}
+{-# LANGUAGE ConstraintKinds            #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+{-# LANGUAGE TypeFamilies               #-}
+{-# LANGUAGE TypeOperators              #-}
+
+module Data.ASN1.Prim
+    ( TagPC(..)
+    , TL
+    , Tag(..)
+    , TagK(..), KnownTag(..)
+    , EncodingRule(..)
+    , isolate64
+
+    , putTagLength
+    , getTagLength
+
+    , getVarInt64
+    , putVarInt64
+    , asPrimitive
+
+    , getVarInteger
+    , putVarInteger
+    ) where
+
+import           Common
+
+import           Data.Binary     as Bin
+import           Data.Binary.Get as Bin
+import           Data.Binary.Put as Bin
+
+data TagPC
+  = Primitive
+  | Constructed
+  deriving (Enum,Eq,Show)
+
+data EncodingRule
+  = BER
+  | CER
+  | DER
+  deriving Eq
+
+isolate64 :: Word64 -> Get a -> Get a
+isolate64 sz64 act
+  | Just sz <- intCastMaybe sz64 = Bin.isolate sz act
+  | otherwise = fail "isolate64: exceeding supported limits"
+
+type TL = (Tag, TagPC, Maybe Word64)
+
+getTagLength :: EncodingRule -> Get (Maybe TL)
+getTagLength r = do
+  eof <- isEmpty
+
+  if eof
+    then pure Nothing
+    else Just <$> do
+      (t,pc) <- getTag r
+      l <- getLength (r /= BER)
+
+      case (r,l,pc) of
+        (_,Nothing,Primitive)    -> fail "indefinite length not allowed for primitive encoding"
+        (DER,Nothing,_)          -> fail "indefinite length encoding not allowed by DER"
+        (CER,Just _,Constructed) -> fail "definite length not allowed for constructed encoding by CER"
+        _                        -> pure ()
+
+      pure (t,pc,l)
+
+putTagLength :: TL -> PutM Word64
+putTagLength (_,Primitive,Nothing) = error "indefinite length not allowed for primitive encoding"
+putTagLength (t,pc,msz)            = (+) <$> putTag t pc <*> putLength msz
+
+getTag :: EncodingRule -> Get (Tag, TagPC)
+getTag _ = do
+  b0 <- getWord8
+  let !pc = if testBit b0 5 then Constructed else Primitive
+      n0 = b0 .&. 0x1f
+
+  !tn <- case n0 of
+          0x1f -> getXTagNum -- long-form tag-number
+          _    -> pure (fromIntegral n0)
+
+  case b0 .&. 0xc0 of
+    0x00 -> pure (Universal   tn, pc)
+    0x40 -> pure (Application tn, pc)
+    0x80 -> pure (Contextual  tn, pc)
+    0xc0 -> pure (Private     tn, pc)
+    _    -> fail "the impossible happened"
+
+putTag :: Tag -> TagPC -> PutM Word64
+putTag t pc = do
+  when (tagNum t >= 31) $ error "putTag: FIXME"
+
+  let w8_cls = case t of
+                 Universal   _ -> 0x00
+                 Application _ -> 0x40
+                 Contextual  _ -> 0x80
+                 Private     _ -> 0xc0
+
+      w8_pc  = case pc of
+                 Constructed -> 0x20
+                 Primitive   -> 0x00
+
+      w8_tn  = fromIntegral (tagNum t)
+
+  putWord8 (w8_cls .|. w8_pc .|. w8_tn)
+  pure 1
+
+getXTagNum :: Get Word64
+getXTagNum = do
+    (more0,n0) <- getWord7
+    let n0' = fromIntegral n0
+
+    when (n0' == 0) $
+      fail "lower 7 bits of the first subsequent tag-number octet shall not all be zero"
+
+    if more0
+      then go n0'
+      else pure n0'
+
+  where
+    go :: Word64 -> Get Word64
+    go !acc = do
+      (mo,o7) <- getWord7
+      let acc' = (acc `shiftL` 7) .|. fromIntegral o7
+
+      when (acc >= 0x0200000000000000) $
+        fail "tag number exceeds 64bit range" -- TODO: investigate whether there's ASN.1 schemas requiring larger tag-numbers
+
+      if mo
+        then go acc'
+        else pure $! acc'
+
+getWord7 :: Get (Bool,Word8)
+getWord7 = do
+  x <- getWord8
+  let n = x .&. 0x7f
+      more = x /= n
+  pure (more, n)
+
+getLength :: Bool -> Get (Maybe Word64) -- 'Nothing' denotes indefinite
+getLength minimal = do
+    xb7 <- getWord7
+    case xb7 of
+      -- definite short-form
+      (False,n)   -> pure $! Just $! fromIntegral n
+
+      -- indefinite
+      (True,0)    -> pure Nothing
+
+      -- reserved
+      (True,0x7f) -> fail "length octet with reserved value 0xff encountered"
+
+      -- definite long-form
+      (True,sz)   -> Just <$> go sz 0
+  where
+    go :: Word8 -> Word64 -> Get Word64
+    go 0 acc
+      | minimal, acc < 0x1f  = fail "length not encoded minimally"
+      | otherwise            = pure acc
+    go sz acc = do
+      when (acc >= 0x0100000000000000) $
+        fail "length exceeds 64bit quantity"
+
+      x <- getWord8
+
+      let acc' = (acc `shiftL` 8) .|. fromIntegral x
+      when (minimal && acc == 0 && x == 0) $
+        fail "length not encoded minimally"
+
+      go (sz-1) acc'
+
+putLength :: Maybe Word64 -> PutM Word64
+putLength Nothing = putWord8 0x80 *> pure 1
+putLength (Just sz)
+  | sz < 0x80 = putWord8 (fromIntegral sz) *> pure 1
+  | otherwise = do
+      let w8s = splitWord64 sz
+          n   = length w8s
+
+      putWord8 (0x80 + fromIntegral n)
+      mapM_ putWord8 w8s
+      pure (1 + fromIntegral n)
+
+
+asPrimitive :: (Word64 -> Get x) -> TL -> Get x
+asPrimitive _ (_,_,Nothing)         = fail "indefinite length not allowed"
+asPrimitive _ (_,Constructed,_)     = fail "must be primitive"
+asPrimitive f (_,Primitive,Just sz) = f sz
+
+----------------------------------------------------------------------------
+
+getInt24be :: Get Int32
+getInt24be = do
+  hi <- getInt8
+  lo <- getWord16be
+  pure $! (fromIntegral hi `shiftL` 16) + fromIntegral lo
+
+getInt40be :: Get Int64
+getInt40be = do
+  hi <- getInt8
+  lo <- getWord32be
+  pure $! (fromIntegral hi `shiftL` 32) + fromIntegral lo
+
+getInt48be :: Get Int64
+getInt48be = do
+  hi <- getInt16be
+  lo <- getWord32be
+  pure $! (fromIntegral hi `shiftL` 32) + fromIntegral lo
+
+getInt56be :: Get Int64
+getInt56be = do
+  hi <- getInt24be
+  lo <- getWord32be
+  pure $! (fromIntegral hi `shiftL` 32) + fromIntegral lo
+
+
+getVarInt64 :: Word64 -> Get Int64
+getVarInt64 = \case
+  0 -> fail "invalid zero-sized INTEGER"
+  1 -> fromIntegral <$> getInt8
+  2 -> fromIntegral <$> getInt16be
+  3 -> fromIntegral <$> getInt24be
+  4 -> fromIntegral <$> getInt32be
+  5 ->                  getInt40be
+  6 ->                  getInt48be
+  7 ->                  getInt56be
+  8 ->                  getInt64be
+  _ -> fail "INTEGER too large for type"
+
+getVarInteger :: Word64 -> Get Integer
+getVarInteger sz
+  | sz <= 8 = toInteger <$> getVarInt64 sz
+  | otherwise = fail "INTEGER: FIXME/TODO"
+
+
+putVarInt64 :: Int64 -> PutM Word64
+putVarInt64 i = do
+    mapM_ Bin.putWord8 w8s
+    pure (fromIntegral $ length w8s)
+  where
+    w8s = splitInt64 i
+
+
+putVarInteger :: Integer -> PutM Word64
+putVarInteger j
+  | Just i <- intCastMaybe j = putVarInt64 i
+  | otherwise = error "putVarInteger: FIXME"
+
+splitInt64 :: Int64 -> [Word8]
+splitInt64 i
+  | i >= 0x80 = goP i False []
+  | i < -0x80 = goN i True  []
+  | otherwise = [fromIntegral i]
+  where
+    goP 0 False acc = acc
+    goP 0 True  acc = 0x00 : acc
+    goP j _  acc = goP (j `shiftR` 8) (w8 >= 0x80) (w8 : acc)
+      where w8 = fromIntegral (j .&. 0xff)
+
+    goN (-1) True  acc = acc
+    goN (-1) False acc = 0xff : acc
+    goN j _        acc = goN (j `shiftR` 8) (w8 >= 0x80) (w8 : acc)
+      where w8 = fromIntegral (j .&. 0xff)
+
+
+splitWord64 :: Word64 -> [Word8]
+splitWord64 i
+  | i > 0xff  = go i []
+  | otherwise = [fromIntegral i]
+  where
+    go 0 acc = acc
+    go j acc = go (j `shiftR` 8) (w8 : acc)
+      where w8 = fromIntegral (j .&. 0xff)
+
+
+----------------------------------------------------------------------------
+
+-- | ASN.1 Tag
+data Tag = Universal   { tagNum :: !Word64 }
+         | Application { tagNum :: !Word64 }
+         | Contextual  { tagNum :: !Word64 }
+         | Private     { tagNum :: !Word64 }
+         deriving (Eq,Ord)
+
+instance Show Tag where
+  show = \case
+     Universal n   -> "[UNIVERSAL "   ++ show n ++ "]"
+     Application n -> "[APPLICATION " ++ show n ++ "]"
+     Contextual n  -> "["             ++ show n ++ "]"
+     Private n     -> "[PRIVATE "     ++ show n ++ "]"
+
+----------------------------------------------------------------------------
+
+-- | Type-level promoted 'Tag'
+data TagK = UNIVERSAL   Nat
+          | APPLICATION Nat
+          | CONTEXTUAL  Nat
+          | PRIVATE     Nat
+
+class KnownTag (tag :: TagK) where
+  tagVal :: Proxy tag -> Tag
+
+instance forall n . (KnownNat n) => KnownTag ('UNIVERSAL n) where
+  tagVal _ = Universal (fromIntegral $ natVal (Proxy :: Proxy n))
+
+instance forall n . (KnownNat n) => KnownTag ('APPLICATION n) where
+  tagVal _ = Application (fromIntegral $ natVal (Proxy :: Proxy n))
+
+instance forall n . (KnownNat n) => KnownTag ('CONTEXTUAL n) where
+  tagVal _ = Contextual (fromIntegral $ natVal (Proxy :: Proxy n))
+
+instance forall n . (KnownNat n) => KnownTag ('PRIVATE n) where
+  tagVal _ = Private (fromIntegral $ natVal (Proxy :: Proxy n))
+
+----------------------------------------------------------------------------
diff --git a/src/Data/Int/Subtypes.hs b/src/Data/Int/Subtypes.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Int/Subtypes.hs
@@ -0,0 +1,199 @@
+-- Copyright (c) 2018-2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE ConstraintKinds      #-}
+{-# LANGUAGE DataKinds            #-}
+{-# LANGUAGE KindSignatures       #-}
+{-# LANGUAGE ScopedTypeVariables  #-}
+{-# LANGUAGE TypeFamilies         #-}
+{-# LANGUAGE TypeOperators        #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Data.Int.Subtypes
+    ( UInt(..), toUInt, fromUInt, uintFromInteger
+    , SInt(..), toSInt, fromSInt, sintFromInteger
+
+    , UIntBounds
+    , SIntBounds
+
+      -- helpers
+    , IsBelowMaxBound
+    , IsAboveMinBoundNeg
+    ) where
+
+import           Common
+
+import           Data.Coerce (coerce)
+
+-- | Unsigned integer sub-type
+newtype UInt (lb :: Nat) (ub :: Nat) t = UInt t
+  deriving (Eq,Ord)
+
+-- | Signed integer sub-type
+--
+-- __NOTE__: Due to lack of negative type-level integer literals the
+-- lower bound is negated, i.e. it expresses a negative magnitude
+newtype SInt (nlb :: Nat) (ub :: Nat) t = SInt t
+  deriving (Eq,Ord)
+
+-- | Coerce integer sub-type into its base-type
+fromUInt :: UInt lb ub t -> t
+fromUInt (UInt i) = i
+
+-- | Coerce integer sub-type into its base-type
+fromSInt :: SInt nlb ub t -> t
+fromSInt (SInt i) = i
+
+instance forall lb ub t . NFData t => NFData (UInt lb ub t) where
+  rnf = coerce (rnf :: t -> ())
+
+instance forall lb ub t . NFData t => NFData (SInt lb ub t) where
+  rnf = coerce (rnf :: t -> ())
+
+instance forall lb ub t . Show t => Show (UInt lb ub t) where
+  show      = coerce (show :: t -> String)
+  showsPrec = coerce (showsPrec :: Int -> t -> ShowS)
+
+instance forall nlb ub t . Show t => Show (SInt nlb ub t) where
+  show      = coerce (show :: t -> String)
+  showsPrec = coerce (showsPrec :: Int -> t -> ShowS)
+
+-- | Constraint encoding type-level invariants for 'UInt'
+type UIntBounds lb ub t = ( KnownNat lb, KnownNat ub, lb <= ub
+                          , IsBelowMaxBound ub (IntBaseType t) ~ 'True)
+
+-- | Constraint encoding type-level invariants for 'SInt'
+type SIntBounds nlb ub t = ( KnownNat nlb, KnownNat ub
+                          , IsAboveMinBoundNeg nlb (IntBaseType t) ~ 'True
+                          , IsBelowMaxBound ub (IntBaseType t) ~ 'True)
+
+type family IsBelowMaxBound (n :: Nat) (t :: IntBaseTypeK) :: Bool where
+  IsBelowMaxBound n ('FixedWordTag b) = n+1 <=? (2^b)
+  IsBelowMaxBound n ('FixedIntTag b)  = n+1 <=? (2^(b-1))
+  IsBelowMaxBound n 'BigIntTag        = 'True
+  IsBelowMaxBound n 'BigWordTag       = 'True
+
+type family IsAboveMinBoundNeg (n :: Nat) (t :: IntBaseTypeK) :: Bool where
+  IsAboveMinBoundNeg n ('FixedWordTag b) = n <=? 0
+  IsAboveMinBoundNeg n ('FixedIntTag b)  = n <=? (2^(b-1))
+  IsAboveMinBoundNeg n 'BigIntTag        = 'True
+  IsAboveMinBoundNeg n 'BigWordTag       = n <=? 0
+
+instance forall lb ub t . (UIntBounds lb ub t, Num t) => Bounded (UInt lb ub t) where
+  minBound = UInt $ fromInteger (natVal (Proxy :: Proxy lb))
+  maxBound = UInt $ fromInteger (natVal (Proxy :: Proxy ub))
+
+instance forall nlb ub t . (SIntBounds nlb ub t, Num t) => Bounded (SInt nlb ub t) where
+  minBound = SInt $ fromInteger (-natVal (Proxy :: Proxy nlb))
+  maxBound = SInt $ fromInteger (natVal (Proxy :: Proxy ub))
+
+----------------------------------------------------------------------------
+
+uintFromInteger :: forall lb ub t . (UIntBounds lb ub t, Num t) => Integer -> Either ArithException (UInt lb ub t)
+uintFromInteger i
+  | i < natVal (Proxy :: Proxy lb) = Left Underflow
+  | i > natVal (Proxy :: Proxy ub) = Left Overflow
+  | otherwise                      = Right i'
+  where
+    i' = UInt (fromInteger i) :: UInt lb ub t
+
+-- | Try to coerce a base-type into its 'UInt' sub-type
+--
+-- If out of range, @'Left' 'Underflow'@ or @'Right' 'Overflow'@ will be returned.
+toUInt :: forall lb ub t . (UIntBounds lb ub t, Num t, Ord t) => t -> Either ArithException (UInt lb ub t)
+toUInt i
+  | i' < minBound = Left Underflow
+  | i' > maxBound = Left Overflow
+  | otherwise     = Right i'
+  where
+    i' = UInt i :: UInt lb ub t
+
+toUInt' :: forall lb ub t . (UIntBounds lb ub t, Num t, Ord t) => t -> UInt lb ub t
+toUInt' = either throw id . toUInt
+
+instance forall lb ub t . (UIntBounds lb ub t, Integral t, Ord t) => Num (UInt lb ub t) where
+  fromInteger     = either throw id . uintFromInteger
+
+  UInt 0 * _      = UInt 0
+  UInt 1 * y      = y
+  _      * UInt 0 = UInt 0
+  x      * UInt 1 = x
+  UInt x * UInt y = fromInteger (toInteger x * toInteger y)
+
+  UInt 0 + y      = y
+  x      + UInt 0 = x
+  UInt x + UInt y = fromInteger (toInteger x + toInteger y)
+
+  x      - UInt 0 = x
+  UInt 0 - y      = negate y
+  UInt x - UInt y = fromInteger (toInteger x - toInteger y)
+
+  negate (UInt 0) = UInt 0
+  negate (UInt _) = throw Underflow
+
+  abs             = id
+
+  signum (UInt 0) = UInt 0
+  signum (UInt _) = toUInt' 1
+
+----------------------------------------------------------------------------
+
+sintFromInteger :: forall nlb ub t . (SIntBounds nlb ub t, Num t) => Integer -> Either ArithException (SInt nlb ub t)
+sintFromInteger i
+  | i < -natVal (Proxy :: Proxy nlb) = Left Underflow
+  | i >  natVal (Proxy :: Proxy ub)  = Left Overflow
+  | otherwise                        = Right i'
+  where
+    i' = SInt (fromInteger i) :: SInt nlb ub t
+
+-- | Try to coerce a base-type into its 'SInt' sub-type
+--
+-- If out of range, @'Left' 'Underflow'@ or @'Right' 'Overflow'@ will be returned.
+toSInt :: forall nlb ub t . (SIntBounds nlb ub t, Num t, Ord t) => t -> Either ArithException (SInt nlb ub t)
+toSInt i
+  | i' < minBound = Left Underflow
+  | i' > maxBound = Left Overflow
+  | otherwise     = Right i'
+  where
+    i' = SInt i :: SInt nlb ub t
+
+toSInt' :: forall nlb ub t . (SIntBounds nlb ub t, Num t, Ord t) => t -> SInt nlb ub t
+toSInt' = either throw id . toSInt
+
+instance forall nlb ub t . (SIntBounds nlb ub t, Integral t, Ord t) => Num (SInt nlb ub t) where
+  fromInteger     = either throw id . sintFromInteger
+
+  SInt 0 * _      = SInt 0
+  SInt 1 * y      = y
+  _      * SInt 0 = SInt 0
+  x      * SInt 1 = x
+  SInt x * SInt y = fromInteger (toInteger x * toInteger y)
+
+  SInt 0 + y      = y
+  x      + SInt 0 = x
+  SInt x + SInt y = fromInteger (toInteger x + toInteger y)
+
+  x      - SInt 0 = x
+  SInt 0 - y      = negate y
+  SInt x - SInt y = fromInteger (toInteger x - toInteger y)
+
+  negate (SInt 0) = SInt 0
+  negate (SInt x) = fromInteger (toInteger x)
+
+  abs    (SInt x) = fromInteger (abs (toInteger x))
+
+  signum (SInt 0) = SInt 0
+  signum (SInt x) = toSInt' (signum x)
diff --git a/src/LDAPv3.hs b/src/LDAPv3.hs
new file mode 100644
--- /dev/null
+++ b/src/LDAPv3.hs
@@ -0,0 +1,1180 @@
+-- Copyright (c) 2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase                 #-}
+{-# LANGUAGE RecordWildCards            #-}
+{-# LANGUAGE TypeOperators              #-}
+
+-- | This module provides a pure Haskell implementation of the /Lightweight Directory Access Protocol (LDAP)/ version 3 as specified in <https://tools.ietf.org/html/rfc4511 RFC4511>.
+--
+-- Serializing and deserializing to and from the wire <https://en.wikipedia.org/wiki/ASN.1 ASN.1> encoding is provided via the 'Bin.Binary' instance of 'LDAPMessage'. For the purpose of implementing network clients and servers, the operations
+--
+-- * 'Bin.encode'
+-- * 'Data.Binary.Get.runGetIncremental'
+--
+-- are most useful.
+--
+module LDAPv3
+    ( -- * LDAPv3 Protocol data structures
+      --
+      -- | The Haskell data structures defined in this module closely follow the protocol specification as laid out in <https://tools.ietf.org/html/rfc4511 RFC4511>.
+      --
+      -- For convenience, the normative <https://en.wikipedia.org/wiki/ASN.1 ASN.1> definitions for each Haskell data type are quoted.
+
+      -- ** Common Elements (<https://tools.ietf.org/html/rfc4511#section-4.1 RFC4511 Section 4.1>)
+
+      -- 4.1.1.  Message Envelope
+      LDAPMessage(..)
+    , MessageID(..)
+    , MaxInt
+    , ProtocolOp(..)
+
+      -- 4.1.2.  String Types
+    , LDAPString
+    , LDAPOID
+      -- 4.1.3.  Distinguished Name and Relative Distinguished Name
+    , LDAPDN
+    , RelativeLDAPDN
+      -- 4.1.4.  Attribute Descriptions
+    , AttributeDescription
+      -- 4.1.5.  Attribute Value
+    , AttributeValue
+      -- 4.1.6.  Attribute Value Assertion
+    , AttributeValueAssertion(..)
+    , AssertionValue
+      -- 4.1.7.  Attribute and PartialAttribute
+    , PartialAttribute(..)
+    , Attribute(..)
+      -- 4.1.8.  Matching Rule Identifier
+    , MatchingRuleId
+      -- 4.1.9.  Result Message
+    , LDAPResult(..)
+    , ResultCode(..)
+      -- 4.1.10.  Referral
+    , Referral
+    , URI
+      -- 4.1.11.  Controls
+    , Controls
+    , Control(..)
+
+      -- ** Bind Operation  (<https://tools.ietf.org/html/rfc4511#section-4.2 RFC4511 Section 4.2>)
+
+    , BindRequest(..)
+    , AuthenticationChoice(..)
+    , SaslCredentials(..)
+    , BindResponse(..)
+
+      -- ** Unbind Operation  (<https://tools.ietf.org/html/rfc4511#section-4.3 RFC4511 Section 4.3>)
+
+    , UnbindRequest
+
+      -- ** Unsolicited Notification  (<https://tools.ietf.org/html/rfc4511#section-4.4 RFC4511 Section 4.4>)
+      --
+      -- | Unsolicited notifications are represented by an 'ExtendedResponse' message with its 'MessageID' set to @0@.
+
+      -- ** Search Operation  (<https://tools.ietf.org/html/rfc4511#section-4.5 RFC4511 Section 4.5>)
+
+    , SearchRequest(..)
+    , Scope(..)
+    , DerefAliases(..)
+    , AttributeSelection
+    , Filter(..)
+    , SubstringFilter(..)
+    , Substring(..)
+    , MatchingRuleAssertion(..)
+
+      -- *** Search Result   (<https://tools.ietf.org/html/rfc4511#section-4.5.2 RFC4511 Section 4.5.2>)
+
+    , SearchResultEntry(..)
+    , PartialAttributeList
+    , SearchResultReference(..)
+    , SearchResultDone
+
+      -- ** Modify Operation   (<https://tools.ietf.org/html/rfc4511#section-4.6 RFC4511 Section 4.6>)
+
+    , ModifyRequest(..)
+    , Change(..)
+    , Operation(..)
+    , ModifyResponse
+
+      -- ** Add Operation   (<https://tools.ietf.org/html/rfc4511#section-4.7 RFC4511 Section 4.7>)
+
+    , AddRequest(..)
+    , AttributeList
+    , AddResponse
+
+      -- ** Delete Operation   (<https://tools.ietf.org/html/rfc4511#section-4.8 RFC4511 Section 4.8>)
+
+    , DelRequest
+    , DelResponse
+
+      -- ** Modify DN Operation   (<https://tools.ietf.org/html/rfc4511#section-4.9 RFC4511 Section 4.9>)
+
+    , ModifyDNRequest(..)
+    , ModifyDNResponse
+
+      -- ** Compare Operation   (<https://tools.ietf.org/html/rfc4511#section-4.10 RFC4511 Section 4.10>)
+
+    , CompareRequest(..)
+    , CompareResponse
+
+      -- ** Abandon Operation   (<https://tools.ietf.org/html/rfc4511#section-4.11 RFC4511 Section 4.11>)
+
+    , AbandonRequest
+
+      -- ** Extended Operation   (<https://tools.ietf.org/html/rfc4511#section-4.12 RFC4511 Section 4.12>)
+
+    , ExtendedRequest(..)
+    , ExtendedResponse(..)
+
+      -- ** Intermediate Response  (<https://tools.ietf.org/html/rfc4511#section-4.13 RFC4511 Section 4.13>)
+
+    , IntermediateResponse(..)
+
+      -- * ASN.1 Helpers
+    , NULL
+    , OCTET_STRING
+    , BOOLEAN_DEFAULT_FALSE(..)
+    , SET(..)
+    , SET1(..)
+
+      -- ** ASN.1 type-level tagging
+    , EXPLICIT(..)
+    , IMPLICIT(..)
+    , TagK(..)
+
+      -- * Unsigned integer sub-type
+    , UIntBounds
+    , UInt
+    , fromUInt
+    , toUInt
+    ) where
+
+import           Common
+import           Data.ASN1
+import           Data.ASN1.Prim
+import           Data.Int.Subtypes
+import           LDAPv3.ResultCode
+
+import qualified Data.Binary       as Bin
+
+----------------------------------------------------------------------------
+-- LDAPv3 protocol
+
+{- | Message Envelope (<https://tools.ietf.org/html/rfc4511#section-4.1.1 RFC4511 Section 4.1.1>)
+
+> LDAPMessage ::= SEQUENCE {
+>      messageID       MessageID,
+>      protocolOp      CHOICE {
+>           bindRequest           BindRequest,
+>           bindResponse          BindResponse,
+>           unbindRequest         UnbindRequest,
+>           searchRequest         SearchRequest,
+>           searchResEntry        SearchResultEntry,
+>           searchResDone         SearchResultDone,
+>           searchResRef          SearchResultReference,
+>           modifyRequest         ModifyRequest,
+>           modifyResponse        ModifyResponse,
+>           addRequest            AddRequest,
+>           addResponse           AddResponse,
+>           delRequest            DelRequest,
+>           delResponse           DelResponse,
+>           modDNRequest          ModifyDNRequest,
+>           modDNResponse         ModifyDNResponse,
+>           compareRequest        CompareRequest,
+>           compareResponse       CompareResponse,
+>           abandonRequest        AbandonRequest,
+>           extendedReq           ExtendedRequest,
+>           extendedResp          ExtendedResponse,
+>           ...,
+>           intermediateResponse  IntermediateResponse },
+>      controls       [0] Controls OPTIONAL }
+
+-}
+data LDAPMessage = LDAPMessage
+  { _LDAPMessage'messageID  :: MessageID
+  , _LDAPMessage'protocolOp :: ProtocolOp
+  , _LDAPMessage'controls   :: Maybe ('CONTEXTUAL 0 `IMPLICIT` Controls)
+  } deriving (Generic,Show,Eq)
+
+-- | Encodes to\/from ASN.1 as per <https://tools.ietf.org/html/rfc4511#section-5.1 RFC4511 Section 5.1>
+instance Bin.Binary LDAPMessage where
+  put = void . toBinaryPut . asn1encode
+  get = toBinaryGet asn1decode
+
+instance ASN1 LDAPMessage where
+  asn1decodeCompOf = LDAPMessage <$> asn1decode <*> asn1decode <*> asn1decode
+  asn1encodeCompOf (LDAPMessage v1 v2 v3) = asn1encodeCompOf (v1,v2,v3)
+
+{- | Message ID (<https://tools.ietf.org/html/rfc4511#section-4.1.1.1 RFC4511 Section 4.1.1.1>)
+
+> MessageID ::= INTEGER (0 ..  maxInt)
+
+-}
+newtype MessageID = MessageID (UInt 0 MaxInt Int32)
+                  deriving (Generic,NFData,Ord,Bounded,ASN1,Show,Eq)
+
+{- | LDAPv3 protocol ASN.1 constant as per <https://tools.ietf.org/html/rfc4511#section-4.1.1 RFC4511 Section 4.1.1>
+
+> maxInt INTEGER ::= 2147483647 -- (2^^31 - 1)
+
+-}
+type MaxInt = 2147483647
+
+-- | @CHOICE@ type inlined in @LDAPMessage.protocolOp@  (<https://tools.ietf.org/html/rfc4511#section-4.1.1 RFC4511 Section 4.1.1>)
+--
+data ProtocolOp
+  = ProtocolOp'bindRequest     BindRequest
+  | ProtocolOp'bindResponse    BindResponse
+  | ProtocolOp'unbindRequest   UnbindRequest
+  | ProtocolOp'searchRequest   SearchRequest
+  | ProtocolOp'searchResEntry  SearchResultEntry
+  | ProtocolOp'searchResDone   SearchResultDone
+  | ProtocolOp'searchResRef    SearchResultReference
+  | ProtocolOp'modifyRequest   ModifyRequest
+  | ProtocolOp'modifyResponse  ModifyResponse
+  | ProtocolOp'addRequest      AddRequest
+  | ProtocolOp'addResponse     AddResponse
+  | ProtocolOp'delRequest      DelRequest
+  | ProtocolOp'delResponse     DelResponse
+  | ProtocolOp'modDNRequest    ModifyDNRequest
+  | ProtocolOp'modDNResponse   ModifyDNResponse
+  | ProtocolOp'compareRequest  CompareRequest
+  | ProtocolOp'compareResponse CompareResponse
+  | ProtocolOp'abandonRequest  AbandonRequest
+  | ProtocolOp'extendedReq     ExtendedRequest
+  | ProtocolOp'extendedResp    ExtendedResponse
+  | ProtocolOp'intermediateResponse  IntermediateResponse
+  deriving (Generic,Show,Eq)
+
+instance NFData ProtocolOp
+
+instance ASN1 ProtocolOp where
+  asn1decode = with'CHOICE
+    [ ProtocolOp'bindRequest    <$> asn1decode
+    , ProtocolOp'bindResponse   <$> asn1decode
+    , ProtocolOp'unbindRequest  <$> asn1decode
+    , ProtocolOp'searchRequest  <$> asn1decode
+    , ProtocolOp'searchResEntry <$> asn1decode
+    , ProtocolOp'searchResDone  <$> asn1decode
+    , ProtocolOp'searchResRef   <$> asn1decode
+    , ProtocolOp'modifyRequest  <$> asn1decode
+    , ProtocolOp'modifyResponse <$> asn1decode
+    , ProtocolOp'addRequest     <$> asn1decode
+    , ProtocolOp'addResponse    <$> asn1decode
+    , ProtocolOp'delRequest     <$> asn1decode
+    , ProtocolOp'delResponse    <$> asn1decode
+    , ProtocolOp'modDNRequest   <$> asn1decode
+    , ProtocolOp'modDNResponse  <$> asn1decode
+    , ProtocolOp'compareRequest <$> asn1decode
+    , ProtocolOp'compareResponse <$> asn1decode
+    , ProtocolOp'abandonRequest <$> asn1decode
+    , ProtocolOp'extendedReq    <$> asn1decode
+    , ProtocolOp'extendedResp   <$> asn1decode
+    , ProtocolOp'intermediateResponse <$> asn1decode
+    ]
+
+  asn1encode = \case
+    ProtocolOp'bindRequest    v -> asn1encode v
+    ProtocolOp'bindResponse   v -> asn1encode v
+    ProtocolOp'unbindRequest  v -> asn1encode v
+    ProtocolOp'searchRequest  v -> asn1encode v
+    ProtocolOp'searchResEntry v -> asn1encode v
+    ProtocolOp'searchResDone  v -> asn1encode v
+    ProtocolOp'searchResRef   v -> asn1encode v
+    ProtocolOp'modifyRequest  v -> asn1encode v
+    ProtocolOp'modifyResponse v -> asn1encode v
+    ProtocolOp'addRequest     v -> asn1encode v
+    ProtocolOp'addResponse    v -> asn1encode v
+    ProtocolOp'delRequest     v -> asn1encode v
+    ProtocolOp'delResponse    v -> asn1encode v
+    ProtocolOp'modDNRequest   v -> asn1encode v
+    ProtocolOp'modDNResponse  v -> asn1encode v
+    ProtocolOp'compareRequest v -> asn1encode v
+    ProtocolOp'compareResponse v -> asn1encode v
+    ProtocolOp'abandonRequest v -> asn1encode v
+    ProtocolOp'extendedReq    v -> asn1encode v
+    ProtocolOp'extendedResp   v -> asn1encode v
+    ProtocolOp'intermediateResponse v -> asn1encode v
+
+----------------------------------------------------------------------------
+
+{- | Controls  (<https://tools.ietf.org/html/rfc4511#section-4.1.11 RFC4511 Section 4.1.11>)
+
+> Controls ::= SEQUENCE OF control Control
+
+-}
+type Controls = [Control]
+
+{- | Control Entry  (<https://tools.ietf.org/html/rfc4511#section-4.1.11 RFC4511 Section 4.1.11>)
+
+> Control ::= SEQUENCE {
+>      controlType             LDAPOID,
+>      criticality             BOOLEAN DEFAULT FALSE,
+>      controlValue            OCTET STRING OPTIONAL }
+
+-}
+data Control = Control
+  { _Control'controlType  :: LDAPOID
+  , _Control'criticality  :: Maybe BOOLEAN_DEFAULT_FALSE
+  , _Control'controlValue :: Maybe OCTET_STRING
+  } deriving (Generic,Show,Eq)
+
+instance NFData Control
+
+instance ASN1 Control where
+  asn1decodeCompOf = Control <$> asn1decode <*> asn1decode <*> asn1decode
+  asn1encodeCompOf (Control v1 v2 v3) = asn1encodeCompOf (v1,v2,v3)
+
+{- | Object identifier  (<https://tools.ietf.org/html/rfc4511#section-4.1.2 RFC4511 Section 4.1.2>)
+
+> LDAPOID ::= OCTET STRING -- Constrained to <numericoid>
+>                          -- [RFC4512]
+
+-}
+type LDAPOID = OCTET_STRING
+
+----------------------------------------------------------------------------
+
+{- | Bind Request  (<https://tools.ietf.org/html/rfc4511#section-4.2 RFC4511 Section 4.2>)
+
+> BindRequest ::= [APPLICATION 0] SEQUENCE {
+>      version                 INTEGER (1 ..  127),
+>      name                    LDAPDN,
+>      authentication          AuthenticationChoice }
+
+-}
+
+data BindRequest = BindRequest
+  { bindRequest'version        :: UInt 1 127 Int8
+  , bindRequest'name           :: LDAPDN
+  , bindRequest'authentication :: AuthenticationChoice
+  } deriving (Generic,Show,Eq)
+
+instance NFData BindRequest
+
+instance ASN1 BindRequest where
+  asn1defTag _ = Application 0
+  asn1decodeCompOf = BindRequest <$> asn1decode <*> asn1decode <*> asn1decode
+  asn1encodeCompOf (BindRequest v1 v2 v3) = asn1encodeCompOf (v1,v2,v3)
+
+----------------------------------------------------------------------------
+
+{- | See 'BindRequest'
+
+> AuthenticationChoice ::= CHOICE {
+>      simple                  [0] OCTET STRING,
+>                              -- 1 and 2 reserved
+>      sasl                    [3] SaslCredentials,
+>      ...  }
+
+-}
+data AuthenticationChoice
+  = AuthenticationChoice'simple  ('CONTEXTUAL 0 `IMPLICIT` OCTET_STRING)
+  | AuthenticationChoice'sasl    ('CONTEXTUAL 3 `IMPLICIT` SaslCredentials)
+  deriving (Generic,Show,Eq)
+
+instance NFData AuthenticationChoice
+
+instance ASN1 AuthenticationChoice where
+  asn1decode = with'CHOICE
+    [ AuthenticationChoice'simple <$> asn1decode
+    , AuthenticationChoice'sasl   <$> asn1decode
+    ]
+
+  asn1encode = \case
+    AuthenticationChoice'simple v -> asn1encode v
+    AuthenticationChoice'sasl   v -> asn1encode v
+
+{- | See 'AuthenticationChoice'
+
+> SaslCredentials ::= SEQUENCE {
+>      mechanism               LDAPString,
+>      credentials             OCTET STRING OPTIONAL }
+
+-}
+data SaslCredentials = SaslCredentials
+  { _SaslCredentials'mechanism   :: LDAPString
+  , _SaslCredentials'credentials :: Maybe OCTET_STRING
+  } deriving (Generic,Show,Eq)
+
+instance NFData SaslCredentials
+
+instance ASN1 SaslCredentials where
+  asn1decodeCompOf = SaslCredentials <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (SaslCredentials v1 v2) = asn1encodeCompOf (v1,v2)
+
+----------------------------------------------------------------------------
+
+{- | Bind Response  (<https://tools.ietf.org/html/rfc4511#section-4.2 RFC4511 Section 4.2>)
+
+> BindResponse ::= [APPLICATION 1] SEQUENCE {
+>      COMPONENTS OF LDAPResult,
+>      serverSaslCreds    [7] OCTET STRING OPTIONAL }
+
+-}
+
+data BindResponse = BindResponse
+  { _BindResponse'LDAPResult      :: LDAPResult
+  , _BindResponse'serverSaslCreds :: Maybe ('CONTEXTUAL 7 `IMPLICIT` OCTET_STRING)
+  } deriving (Generic,Show,Eq)
+
+instance NFData BindResponse
+
+instance ASN1 BindResponse where
+  asn1defTag _ = Application 1
+  asn1decodeCompOf = do
+    _BindResponse'LDAPResult      <- asn1decodeCompOf
+    _BindResponse'serverSaslCreds <- asn1decode
+    pure BindResponse{..}
+
+  asn1encodeCompOf (BindResponse{..})
+    = enc'SEQUENCE_COMPS [ asn1encodeCompOf _BindResponse'LDAPResult
+                         , asn1encode       _BindResponse'serverSaslCreds
+                         ]
+
+----------------------------------------------------------------------------
+
+{- | Unbind Operation  (<https://tools.ietf.org/html/rfc4511#section-4.3 RFC4511 Section 4.3>)
+
+> UnbindRequest ::= [APPLICATION 2] NULL
+
+-}
+type UnbindRequest = ('APPLICATION 2 `IMPLICIT` NULL)
+
+----------------------------------------------------------------------------
+
+{- | Search Request  (<https://tools.ietf.org/html/rfc4511#section-4.5.1 RFC4511 Section 4.5.1>)
+
+> SearchRequest ::= [APPLICATION 3] SEQUENCE {
+>      baseObject      LDAPDN,
+>      scope           ENUMERATED {
+>           baseObject              (0),
+>           singleLevel             (1),
+>           wholeSubtree            (2),
+>           ...  },
+>      derefAliases    ENUMERATED {
+>           neverDerefAliases       (0),
+>           derefInSearching        (1),
+>           derefFindingBaseObj     (2),
+>           derefAlways             (3) },
+>      sizeLimit       INTEGER (0 ..  maxInt),
+>      timeLimit       INTEGER (0 ..  maxInt),
+>      typesOnly       BOOLEAN,
+>      filter          Filter,
+>      attributes      AttributeSelection }
+
+-}
+data SearchRequest = SearchRequest
+  { _SearchRequest'baseObject   :: LDAPDN
+  , _SearchRequest'scope        :: Scope
+  , _SearchRequest'derefAliases :: DerefAliases
+  , _SearchRequest'sizeLimit    :: (UInt 0 MaxInt Int32)
+  , _SearchRequest'timeLimit    :: (UInt 0 MaxInt Int32)
+  , _SearchRequest'typesOnly    :: Bool
+  , _SearchRequest'filter       :: Filter
+  , _SearchRequest'attributes   :: AttributeSelection
+  } deriving (Generic,Show,Eq)
+
+instance NFData SearchRequest
+
+{- | See 'SearchRequest'
+
+> AttributeSelection ::= SEQUENCE OF selector LDAPString
+>                -- The LDAPString is constrained to
+>                -- <attributeSelector> in Section 4.5.1.8
+
+-}
+type AttributeSelection = [LDAPString]
+
+instance ASN1 SearchRequest where
+  asn1decode = implicit (Application 3) $ with'SEQUENCE $ do
+    _SearchRequest'baseObject   <- asn1decode
+    _SearchRequest'scope        <- asn1decode
+    _SearchRequest'derefAliases <- asn1decode
+    _SearchRequest'sizeLimit    <- asn1decode
+    _SearchRequest'timeLimit    <- asn1decode
+    _SearchRequest'typesOnly    <- asn1decode
+    _SearchRequest'filter       <- asn1decode
+    _SearchRequest'attributes   <- asn1decode
+
+    pure SearchRequest{..}
+
+  asn1encode (SearchRequest v1 v2 v3 v4 v5 v6 v7 v8)
+    = retag (Application 3) $
+      enc'SEQUENCE [ asn1encode v1
+                   , asn1encode v2
+                   , asn1encode v3
+                   , asn1encode v4
+                   , asn1encode v5
+                   , asn1encode v6
+                   , asn1encode v7
+                   , asn1encode v8
+                   ]
+
+-- | See 'SearchRequest'  (<https://tools.ietf.org/html/rfc4511#section-4.5.1.2 RFC4511 Section 4.5.1.2>)
+data Scope
+  = Scope'baseObject
+  | Scope'singleLevel
+  | Scope'wholeSubtree
+  deriving (Generic,Bounded,Enum,Show,Eq)
+
+instance NFData Scope where
+  rnf = rwhnf
+
+instance ASN1 Scope where
+  asn1decode = dec'BoundedEnum
+  asn1encode = enc'BoundedEnum
+
+-- | See 'SearchRequest'  (<https://tools.ietf.org/html/rfc4511#section-4.5.1.3 RFC4511 Section 4.5.1.3>)
+data DerefAliases
+  = DerefAliases'neverDerefAliases
+  | DerefAliases'derefInSearching
+  | DerefAliases'derefFindingBaseObj
+  | DerefAliases'derefAlways
+  deriving (Generic,Bounded,Enum,Show,Eq)
+
+instance NFData DerefAliases where
+  rnf = rwhnf
+
+instance ASN1 DerefAliases where
+  asn1decode = dec'BoundedEnum
+  asn1encode = enc'BoundedEnum
+
+{- | Search Filter  (<https://tools.ietf.org/html/rfc4511#section-4.5.1.7 RFC4511 Section 4.5.1.7>)
+
+> Filter ::= CHOICE {
+>      and             [0] SET SIZE (1..MAX) OF filter Filter,
+>      or              [1] SET SIZE (1..MAX) OF filter Filter,
+>      not             [2] Filter,
+>      equalityMatch   [3] AttributeValueAssertion,
+>      substrings      [4] SubstringFilter,
+>      greaterOrEqual  [5] AttributeValueAssertion,
+>      lessOrEqual     [6] AttributeValueAssertion,
+>      present         [7] AttributeDescription,
+>      approxMatch     [8] AttributeValueAssertion,
+>      extensibleMatch [9] MatchingRuleAssertion,
+>      ...  }
+
+-}
+data Filter
+  = Filter'and             ('CONTEXTUAL 0 `IMPLICIT` SET1 Filter)
+  | Filter'or              ('CONTEXTUAL 1 `IMPLICIT` SET1 Filter)
+  | Filter'not             ('CONTEXTUAL 2 `EXPLICIT` Filter)
+  | Filter'equalityMatch   ('CONTEXTUAL 3 `IMPLICIT` AttributeValueAssertion)
+  | Filter'substrings      ('CONTEXTUAL 4 `IMPLICIT` SubstringFilter)
+  | Filter'greaterOrEqual  ('CONTEXTUAL 5 `IMPLICIT` AttributeValueAssertion)
+  | Filter'lessOrEqual     ('CONTEXTUAL 6 `IMPLICIT` AttributeValueAssertion)
+  | Filter'present         ('CONTEXTUAL 7 `IMPLICIT` AttributeDescription)
+  | Filter'approxMatch     ('CONTEXTUAL 8 `IMPLICIT` AttributeValueAssertion)
+  | Filter'extensibleMatch ('CONTEXTUAL 9 `IMPLICIT` MatchingRuleAssertion)
+  deriving (Generic,Show,Eq)
+
+instance NFData Filter
+
+instance ASN1 Filter where
+  asn1decode = with'CHOICE
+    [ Filter'and             <$> asn1decode
+    , Filter'or              <$> asn1decode
+    , Filter'not             <$> asn1decode
+    , Filter'equalityMatch   <$> asn1decode
+    , Filter'substrings      <$> asn1decode
+    , Filter'greaterOrEqual  <$> asn1decode
+    , Filter'lessOrEqual     <$> asn1decode
+    , Filter'present         <$> asn1decode
+    , Filter'approxMatch     <$> asn1decode
+    , Filter'extensibleMatch <$> asn1decode
+    ]
+
+  asn1encode = \case
+    Filter'and             v -> asn1encode v
+    Filter'or              v -> asn1encode v
+    Filter'not             v -> asn1encode v
+    Filter'equalityMatch   v -> asn1encode v
+    Filter'substrings      v -> asn1encode v
+    Filter'greaterOrEqual  v -> asn1encode v
+    Filter'lessOrEqual     v -> asn1encode v
+    Filter'present         v -> asn1encode v
+    Filter'approxMatch     v -> asn1encode v
+    Filter'extensibleMatch v -> asn1encode v
+
+{- | Attribute Descriptions  (<https://tools.ietf.org/html/rfc4511#section-4.1.4 RFC4511 Section 4.1.4>)
+
+> AttributeDescription ::= LDAPString
+>                         -- Constrained to <attributedescription>
+>                         -- [RFC4512]
+
+-}
+type AttributeDescription = LDAPString
+
+{- | Attribute Value  (<https://tools.ietf.org/html/rfc4511#section-4.1.5 RFC4511 Section 4.1.5>)
+
+> AttributeValue ::= OCTET STRING
+
+-}
+type AttributeValue = OCTET_STRING
+
+{- | Attribute Value Assertion  (<https://tools.ietf.org/html/rfc4511#section-4.1.6 RFC4511 Section 4.1.6>)
+
+> AttributeValueAssertion ::= SEQUENCE {
+>      attributeDesc   AttributeDescription,
+>      assertionValue  AssertionValue }
+
+-}
+data AttributeValueAssertion = AttributeValueAssertion
+  { _AttributeValueAssertion'attributeDesc  :: AttributeDescription
+  , _AttributeValueAssertion'assertionValue :: AssertionValue
+  } deriving (Generic,Show,Eq)
+
+instance NFData AttributeValueAssertion
+
+-- | > AssertionValue ::= OCTET STRING
+type AssertionValue = OCTET_STRING
+
+instance ASN1 AttributeValueAssertion where
+  asn1decodeCompOf = AttributeValueAssertion <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (AttributeValueAssertion v1 v2) = asn1encodeCompOf (v1,v2)
+
+{- | Substring 'Filter'  (<https://tools.ietf.org/html/rfc4511#section-4.5.1.7.2 RFC4511 Section 4.5.1.7.2>)
+
+> SubstringFilter ::= SEQUENCE {
+>      type           AttributeDescription,
+>      substrings     SEQUENCE SIZE (1..MAX) OF substring CHOICE {
+>           initial [0] AssertionValue,  -- can occur at most once
+>           any     [1] AssertionValue,
+>           final   [2] AssertionValue } -- can occur at most once
+>      }
+
+__NOTE__: The additional invariants imposed on the ordering and occurence counts of the @initial@ and @final@ entries MUST currently be enforced by the consumer of this library. Future versions of this library might change to enforce these invariants at the type-level.
+
+Specifically, the invariant stated by the specification is:
+
+/There SHALL be at most one @initial@ and at most one @final@ in the @substrings@ of a SubstringFilter.  If @initial@ is present, it SHALL be the first element of @substrings@.  If @final@ is present, it SHALL be the last element of @substrings@./
+
+-}
+data SubstringFilter = SubstringFilter
+  { _SubstringFilter'type       :: AttributeDescription
+  , _SubstringFilter'substrings :: NonEmpty Substring
+  } deriving (Generic,Show,Eq)
+
+instance NFData SubstringFilter
+
+instance ASN1 SubstringFilter where
+  asn1decodeCompOf = SubstringFilter <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (SubstringFilter v1 v2) = asn1encodeCompOf (v1,v2)
+
+-- | See 'SubstringFilter'
+data Substring
+  = Substring'initial ('CONTEXTUAL 0 `IMPLICIT` AssertionValue) -- ^ may occur at most once; must be first element if present
+  | Substring'any     ('CONTEXTUAL 1 `IMPLICIT` AssertionValue)
+  | Substring'final   ('CONTEXTUAL 2 `IMPLICIT` AssertionValue) -- ^ may occur at most once; must be last element if present
+  deriving (Generic,Show,Eq)
+
+instance NFData Substring
+
+instance ASN1 Substring where
+  asn1decode = with'CHOICE
+    [ Substring'initial <$> asn1decode
+    , Substring'any     <$> asn1decode
+    , Substring'final   <$> asn1decode
+    ]
+
+  asn1encode = \case
+    Substring'initial v -> asn1encode v
+    Substring'any     v -> asn1encode v
+    Substring'final   v -> asn1encode v
+
+
+{- | Matching Rule Identifier  (<https://tools.ietf.org/html/rfc4511#section-4.1.8 RFC4511 Section 4.1.8>)
+
+> MatchingRuleId ::= LDAPString
+
+-}
+type MatchingRuleId = LDAPString
+
+{- | See 'SearchRequest' 'Filter'
+
+> MatchingRuleAssertion ::= SEQUENCE {
+>      matchingRule    [1] MatchingRuleId OPTIONAL,
+>      type            [2] AttributeDescription OPTIONAL,
+>      matchValue      [3] AssertionValue,
+>      dnAttributes    [4] BOOLEAN DEFAULT FALSE }
+
+-}
+data MatchingRuleAssertion = MatchingRuleAssertion
+  { _MatchingRuleAssertion'matchingRule :: Maybe ('CONTEXTUAL 1 `IMPLICIT` MatchingRuleId)
+  , _MatchingRuleAssertion'type         :: Maybe ('CONTEXTUAL 2 `IMPLICIT` AttributeDescription)
+  , _MatchingRuleAssertion'matchValue   ::       ('CONTEXTUAL 3 `IMPLICIT` AssertionValue)
+  , _MatchingRuleAssertion'dnAttributes :: Maybe ('CONTEXTUAL 4 `IMPLICIT` BOOLEAN_DEFAULT_FALSE)
+  } deriving (Generic,Show,Eq)
+
+instance NFData MatchingRuleAssertion
+
+instance ASN1 MatchingRuleAssertion where
+  asn1decodeCompOf = MatchingRuleAssertion <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+  asn1encodeCompOf (MatchingRuleAssertion v1 v2 v3 v4) = asn1encodeCompOf (v1,v2,v3,v4)
+
+----------------------------------------------------------------------------
+
+{- | Search Result Continuation Reference  (<https://tools.ietf.org/html/rfc4511#section-4.5.3 RFC4511 Section 4.5.3>)
+
+> SearchResultReference ::= [APPLICATION 19] SEQUENCE
+>                           SIZE (1..MAX) OF uri URI
+
+-}
+
+newtype SearchResultReference = SearchResultReference (NonEmpty URI)
+  deriving (Generic,NFData,Show,Eq)
+
+instance ASN1 SearchResultReference where
+  asn1defTag _ = Application 19 -- not used
+  asn1decode = SearchResultReference <$> (Application 19 `implicit` asn1decode)
+  asn1encode (SearchResultReference v) = retag (Application 19) $ asn1encode v
+
+----------------------------------------------------------------------------
+
+{- | Search Result Entry  (<https://tools.ietf.org/html/rfc4511#section-4.5.2 RFC4511 Section 4.5.2>)
+
+> SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
+>      objectName      LDAPDN,
+>      attributes      PartialAttributeList }
+
+-}
+data SearchResultEntry = SearchResultEntry
+  { _SearchResultEntry'objectName :: LDAPDN
+  , _SearchResultEntry'attributes :: PartialAttributeList
+  } deriving (Generic,Show,Eq)
+
+instance NFData SearchResultEntry
+
+instance ASN1 SearchResultEntry where
+  asn1defTag _ = Application 4
+  asn1decodeCompOf = SearchResultEntry <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (SearchResultEntry v1 v2) = asn1encodeCompOf (v1,v2)
+
+{- | See 'SearchResultEntry'
+
+> PartialAttributeList ::= SEQUENCE OF
+>                      partialAttribute PartialAttribute
+
+-}
+type PartialAttributeList = [PartialAttribute]
+
+{- | Partial Attribute  (<https://tools.ietf.org/html/rfc4511#section-4.1.7 RFC4511 Section 4.1.7>)
+
+> PartialAttribute ::= SEQUENCE {
+>      type       AttributeDescription,
+>      vals       SET OF value AttributeValue }
+
+-}
+data PartialAttribute = PartialAttribute
+  { _PartialAttribute'type :: AttributeDescription
+  , _PartialAttribute'vals :: SET AttributeValue
+  } deriving (Generic,Show,Eq)
+
+instance NFData PartialAttribute
+
+instance ASN1 PartialAttribute where
+  asn1decodeCompOf = PartialAttribute <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (PartialAttribute v1 v2) = asn1encodeCompOf (v1,v2)
+
+
+{- | Attribute  (<https://tools.ietf.org/html/rfc4511#section-4.1.7 RFC4511 Section 4.1.7>)
+
+> Attribute ::= PartialAttribute(WITH COMPONENTS {
+>      ...,
+>      vals (SIZE(1..MAX))})
+
+-}
+data Attribute = Attribute
+  { _Attribute'type :: AttributeDescription
+  , _Attribute'vals :: SET1 AttributeValue
+  } deriving (Generic,Show,Eq)
+
+instance NFData Attribute
+
+instance ASN1 Attribute where
+  asn1decodeCompOf = Attribute <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (Attribute v1 v2) = asn1encodeCompOf (v1,v2)
+
+----------------------------------------------------------------------------
+
+{- | Search Result Done  (<https://tools.ietf.org/html/rfc4511#section-4.5.2 RFC4511 Section 4.5.2>)
+
+> SearchResultDone ::= [APPLICATION 5] LDAPResult
+
+-}
+type SearchResultDone = ('APPLICATION 5 `IMPLICIT` LDAPResult)
+
+----------------------------------------------------------------------------
+
+{- | Result Message  (<https://tools.ietf.org/html/rfc4511#section-4.1.9 RFC4511 Section 4.1.9>)
+
+> LDAPResult ::= SEQUENCE {
+>      resultCode         ENUMERATED {
+>           success                      (0),
+>           operationsError              (1),
+>           protocolError                (2),
+>           timeLimitExceeded            (3),
+>           sizeLimitExceeded            (4),
+>           compareFalse                 (5),
+>           compareTrue                  (6),
+>           authMethodNotSupported       (7),
+>           strongerAuthRequired         (8),
+>                -- 9 reserved --
+>           referral                     (10),
+>           adminLimitExceeded           (11),
+>           unavailableCriticalExtension (12),
+>           confidentialityRequired      (13),
+>           saslBindInProgress           (14),
+>           noSuchAttribute              (16),
+>           undefinedAttributeType       (17),
+>           inappropriateMatching        (18),
+>           constraintViolation          (19),
+>           attributeOrValueExists       (20),
+>           invalidAttributeSyntax       (21),
+>                -- 22-31 unused --
+>           noSuchObject                 (32),
+>           aliasProblem                 (33),
+>           invalidDNSyntax              (34),
+>                -- 35 reserved for undefined isLeaf --
+>           aliasDereferencingProblem    (36),
+>                -- 37-47 unused --
+>           inappropriateAuthentication  (48),
+>           invalidCredentials           (49),
+>           insufficientAccessRights     (50),
+>           busy                         (51),
+>           unavailable                  (52),
+>           unwillingToPerform           (53),
+>           loopDetect                   (54),
+>                -- 55-63 unused --
+>           namingViolation              (64),
+>           objectClassViolation         (65),
+>           notAllowedOnNonLeaf          (66),
+>           notAllowedOnRDN              (67),
+>           entryAlreadyExists           (68),
+>           objectClassModsProhibited    (69),
+>                -- 70 reserved for CLDAP --
+>           affectsMultipleDSAs          (71),
+>                -- 72-79 unused --
+>           other                        (80),
+>           ...  },
+>      matchedDN          LDAPDN,
+>      diagnosticMessage  LDAPString,
+>      referral           [3] Referral OPTIONAL }
+
+-}
+data LDAPResult = LDAPResult
+  { _LDAPResult'resultCode        :: ResultCode
+  , _LDAPResult'matchedDN         :: LDAPDN
+  , _LDAPResult'diagnosticMessage :: LDAPString
+  , _LDAPResult'referral          :: Maybe ('CONTEXTUAL 3 `IMPLICIT` Referral)
+  } deriving (Generic,Show,Eq)
+
+instance NFData LDAPResult
+
+{- | Referral result code  (<https://tools.ietf.org/html/rfc4511#section-4.1.10 RFC4511 Section 4.1.10>)
+
+> Referral ::= SEQUENCE SIZE (1..MAX) OF uri URI
+
+-}
+type Referral = ('CONTEXTUAL 3 `IMPLICIT` NonEmpty URI)
+
+{- |
+
+> URI ::= LDAPString     -- limited to characters permitted in
+>                        -- URIs
+
+-}
+type URI = LDAPString
+
+instance ASN1 LDAPResult where
+  asn1decodeCompOf = do
+    _LDAPResult'resultCode        <- asn1decode
+    _LDAPResult'matchedDN         <- asn1decode
+    _LDAPResult'diagnosticMessage <- asn1decode
+    _LDAPResult'referral          <- asn1decode
+    pure LDAPResult{..}
+  asn1encodeCompOf (LDAPResult v1 v2 v3 v4) = asn1encodeCompOf (v1,v2,v3,v4)
+
+{- | String Type  (<https://tools.ietf.org/html/rfc4511#section-4.1.2 RFC4511 Section 4.1.2>)
+
+> LDAPString ::= OCTET STRING -- UTF-8 encoded,
+>                             -- [ISO10646] characters
+
+-}
+type LDAPString = ShortText
+
+{- | Distinguished Name  (<https://tools.ietf.org/html/rfc4511#section-4.1.3 RFC4511 Section 4.1.3>)
+
+> LDAPDN ::= LDAPString -- Constrained to <distinguishedName>
+>                       -- [RFC4514]
+
+-}
+type LDAPDN = LDAPString
+
+{- | Relative Distinguished Name  (<https://tools.ietf.org/html/rfc4511#section-4.1.3 RFC4511 Section 4.1.3>)
+
+> RelativeLDAPDN ::= LDAPString -- Constrained to <name-component>
+>                               -- [RFC4514]
+
+-}
+type RelativeLDAPDN = LDAPString
+
+{- | Modify Operation  (<https://tools.ietf.org/html/rfc4511#section-4.6 RFC4511 Section 4.6>)
+
+> ModifyRequest ::= [APPLICATION 6] SEQUENCE {
+>      object          LDAPDN,
+>      changes         SEQUENCE OF change SEQUENCE {
+>           operation       ENUMERATED {
+>                add     (0),
+>                delete  (1),
+>                replace (2),
+>                ...  },
+>           modification    PartialAttribute } }
+
+-}
+data ModifyRequest = ModifyRequest
+  { _ModifyRequest'object  :: LDAPDN
+  , _ModifyRequest'changes :: [Change]
+  } deriving (Generic,Show,Eq)
+
+instance NFData ModifyRequest
+
+instance ASN1 ModifyRequest where
+  asn1defTag _ = Application 6
+  asn1decodeCompOf = ModifyRequest <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (ModifyRequest v1 v2) = asn1encodeCompOf (v1,v2)
+
+-- | See 'ModifyRequest'
+data Change = Change
+  { _Change'operation    :: Operation
+  , _Change'modification :: PartialAttribute
+  } deriving (Generic,Show,Eq)
+
+instance NFData Change
+
+instance ASN1 Change where
+  asn1decodeCompOf = Change <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (Change v1 v2) = asn1encodeCompOf (v1,v2)
+
+-- | See 'ModifyRequest' and 'Change'
+data Operation
+  = Operation'add
+  | Operation'delete
+  | Operation'replace
+  deriving (Generic,Bounded,Enum,Show,Eq)
+
+instance NFData Operation where
+  rnf = rwhnf
+
+instance ASN1 Operation where
+  asn1decode = dec'BoundedEnum
+  asn1encode = enc'BoundedEnum
+
+
+{- | Modify Response  (<https://tools.ietf.org/html/rfc4511#section-4.6 RFC4511 Section 4.6>)
+
+> ModifyResponse ::= [APPLICATION 7] LDAPResult
+
+-}
+type ModifyResponse = ('APPLICATION 7 `IMPLICIT` LDAPResult)
+
+{- | Add Operation  (<https://tools.ietf.org/html/rfc4511#section-4.7 RFC4511 Section 4.7>)
+
+> AddRequest ::= [APPLICATION 8] SEQUENCE {
+>      entry           LDAPDN,
+>      attributes      AttributeList }
+
+-}
+data AddRequest = AddRequest
+  { _AddRequest'entry      :: LDAPDN
+  , _AddRequest'attributes :: AttributeList
+  } deriving (Generic,Show,Eq)
+
+instance NFData AddRequest
+
+instance ASN1 AddRequest where
+  asn1defTag _ = Application 8
+  asn1decodeCompOf = AddRequest <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (AddRequest v1 v2) = asn1encodeCompOf (v1,v2)
+
+{- | Attribute List
+
+> AttributeList ::= SEQUENCE OF attribute Attribute
+
+-}
+type AttributeList = [Attribute]
+
+{- | Add Response  (<https://tools.ietf.org/html/rfc4511#section-4.7 RFC4511 Section 4.7>)
+
+> AddResponse ::= [APPLICATION 9] LDAPResult
+
+-}
+type AddResponse = ('APPLICATION 9 `IMPLICIT` LDAPResult)
+
+
+{- | Delete Operation  (<https://tools.ietf.org/html/rfc4511#section-4.8 RFC4511 Section 4.8>)
+
+> DelRequest ::= [APPLICATION 10] LDAPDN
+
+-}
+type DelRequest = ('APPLICATION 10 `IMPLICIT` LDAPDN)
+
+{- | Delete Response  (<https://tools.ietf.org/html/rfc4511#section-4.8 RFC4511 Section 4.8>)
+
+> DelResponse ::= [APPLICATION 11] LDAPResult
+
+-}
+type DelResponse = ('APPLICATION 11 `IMPLICIT` LDAPResult)
+
+{- | Modify DN Operation  (<https://tools.ietf.org/html/rfc4511#section-4.9 RFC4511 Section 4.9>)
+
+ModifyDNRequest ::= [APPLICATION 12] SEQUENCE {
+     entry           LDAPDN,
+     newrdn          RelativeLDAPDN,
+     deleteoldrdn    BOOLEAN,
+     newSuperior     [0] LDAPDN OPTIONAL }
+
+-}
+data ModifyDNRequest = ModifyDNRequest
+  { _ModifyDNRequest'entry        :: LDAPDN
+  , _ModifyDNRequest'newrdn       :: RelativeLDAPDN
+  , _ModifyDNRequest'deleteoldrdn :: Bool
+  , _ModifyDNRequest'newSuperior  :: Maybe ('CONTEXTUAL 0 `IMPLICIT` LDAPDN)
+  } deriving (Generic,Show,Eq)
+
+instance NFData ModifyDNRequest
+
+instance ASN1 ModifyDNRequest where
+  asn1defTag _ = Application 12
+  asn1decodeCompOf = ModifyDNRequest <$> asn1decode <*> asn1decode <*> asn1decode <*> asn1decode
+  asn1encodeCompOf (ModifyDNRequest v1 v2 v3 v4) = asn1encodeCompOf (v1,v2,v3,v4)
+
+
+{- | Modify DN Response  (<https://tools.ietf.org/html/rfc4511#section-4.9 RFC4511 Section 4.9>)
+
+> ModifyDNResponse ::= [APPLICATION 13] LDAPResult
+
+-}
+type ModifyDNResponse = ('APPLICATION 13 `IMPLICIT` LDAPResult)
+
+
+{- | Compare Operation  (<https://tools.ietf.org/html/rfc4511#section-4.10 RFC4511 Section 4.10>)
+
+> CompareRequest ::= [APPLICATION 14] SEQUENCE {
+>      entry           LDAPDN,
+>      ava             AttributeValueAssertion }
+
+-}
+data CompareRequest = CompareRequest
+  { _CompareRequest'entry :: LDAPDN
+  , _CompareRequest'ava   :: AttributeValueAssertion
+  } deriving (Generic,Show,Eq)
+
+instance NFData CompareRequest
+
+instance ASN1 CompareRequest where
+  asn1defTag _ = Application 14
+  asn1decodeCompOf = CompareRequest <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (CompareRequest v1 v2) = asn1encodeCompOf (v1,v2)
+
+{- | Compare Response  (<https://tools.ietf.org/html/rfc4511#section-4.10 RFC4511 Section 4.10>)
+
+> CompareResponse ::= [APPLICATION 15] LDAPResult
+
+-}
+type CompareResponse = ('APPLICATION 15 `IMPLICIT` LDAPResult)
+
+
+{- | Abandon Operation  (<https://tools.ietf.org/html/rfc4511#section-4.11 RFC4511 Section 4.11>)
+
+> AbandonRequest ::= [APPLICATION 16] MessageID
+
+-}
+type AbandonRequest = ('APPLICATION 16 `IMPLICIT` MessageID)
+
+{- | Extended Request  (<https://tools.ietf.org/html/rfc4511#section-4.12 RFC4511 Section 4.12>)
+
+> ExtendedRequest ::= [APPLICATION 23] SEQUENCE {
+>      requestName      [0] LDAPOID,
+>      requestValue     [1] OCTET STRING OPTIONAL }
+
+-}
+data ExtendedRequest = ExtendedRequest
+  { _ExtendedRequest'responseName  ::       ('CONTEXTUAL 0 `IMPLICIT` LDAPOID)
+  , _ExtendedRequest'responseValue :: Maybe ('CONTEXTUAL 1 `IMPLICIT` OCTET_STRING)
+  } deriving (Generic,Show,Eq)
+
+instance NFData ExtendedRequest
+
+instance ASN1 ExtendedRequest where
+  asn1defTag _ = Application 23
+  asn1decodeCompOf = ExtendedRequest <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (ExtendedRequest v1 v2) = asn1encodeCompOf (v1,v2)
+
+{- | Extended Response  (<https://tools.ietf.org/html/rfc4511#section-4.12 RFC4511 Section 4.12>)
+
+> ExtendedResponse ::= [APPLICATION 24] SEQUENCE {
+>      COMPONENTS OF LDAPResult,
+>      responseName     [10] LDAPOID OPTIONAL,
+>      responseValue    [11] OCTET STRING OPTIONAL }
+
+-}
+data ExtendedResponse = ExtendedResponse
+  { _ExtendedResponse'LDAPResult    :: LDAPResult
+  , _ExtendedResponse'responseName  :: Maybe ('CONTEXTUAL 10 `IMPLICIT` LDAPOID)
+  , _ExtendedResponse'responseValue :: Maybe ('CONTEXTUAL 11 `IMPLICIT` OCTET_STRING)
+  } deriving (Generic,Show,Eq)
+
+instance NFData ExtendedResponse
+
+instance ASN1 ExtendedResponse where
+  asn1defTag _ = Application 24
+  asn1decodeCompOf = do
+    _ExtendedResponse'LDAPResult    <- asn1decodeCompOf
+    _ExtendedResponse'responseName  <- asn1decode
+    _ExtendedResponse'responseValue <- asn1decode
+    pure ExtendedResponse{..}
+
+  asn1encodeCompOf (ExtendedResponse{..})
+    = enc'SEQUENCE_COMPS [ asn1encodeCompOf _ExtendedResponse'LDAPResult
+                         , asn1encode       _ExtendedResponse'responseName
+                         , asn1encode       _ExtendedResponse'responseValue
+                         ]
+
+
+{- | Intermediate Response  (<https://tools.ietf.org/html/rfc4511#section-4.13 RFC4511 Section 4.13>)
+
+> IntermediateResponse ::= [APPLICATION 25] SEQUENCE {
+>         responseName     [0] LDAPOID OPTIONAL,
+>         responseValue    [1] OCTET STRING OPTIONAL }
+
+-}
+data IntermediateResponse = IntermediateResponse
+  { _IntermediateResponse'responseName  :: Maybe ('CONTEXTUAL 0 `IMPLICIT` LDAPOID)
+  , _IntermediateResponse'responseValue :: Maybe ('CONTEXTUAL 1 `IMPLICIT` OCTET_STRING)
+  } deriving (Generic,Show,Eq)
+
+instance NFData IntermediateResponse
+
+instance ASN1 IntermediateResponse where
+  asn1defTag _ = Application 25
+  asn1decodeCompOf = IntermediateResponse <$> asn1decode <*> asn1decode
+  asn1encodeCompOf (IntermediateResponse v1 v2) = asn1encodeCompOf (v1,v2)
diff --git a/src/LDAPv3/ResultCode.hs b/src/LDAPv3/ResultCode.hs
new file mode 100644
--- /dev/null
+++ b/src/LDAPv3/ResultCode.hs
@@ -0,0 +1,140 @@
+-- Copyright (c) 2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE DeriveGeneric #-}
+
+module LDAPv3.ResultCode (ResultCode(..)) where
+
+import           Common
+import           Data.ASN1
+import qualified Data.Map.Strict as Map
+import           Data.Tuple      (swap)
+
+fromEnumeratedMap :: Map.Map ResultCode Int64
+fromEnumeratedMap = Map.fromAscList mapping
+
+toEnumeratedMap :: Map.Map Int64 ResultCode
+toEnumeratedMap = Map.fromAscList (swap <$> mapping)
+
+mapping :: [(ResultCode,Int64)]
+mapping = [ (ResultCode'success                      , 0)
+          , (ResultCode'operationsError              , 1)
+          , (ResultCode'protocolError                , 2)
+          , (ResultCode'timeLimitExceeded            , 3)
+          , (ResultCode'sizeLimitExceeded            , 4)
+          , (ResultCode'compareFalse                 , 5)
+          , (ResultCode'compareTrue                  , 6)
+          , (ResultCode'authMethodNotSupported       , 7)
+          , (ResultCode'strongerAuthRequired         , 8)
+             -- 9 reserved --
+          , (ResultCode'referral                     ,10)
+          , (ResultCode'adminLimitExceeded           ,11)
+          , (ResultCode'unavailableCriticalExtension ,12)
+          , (ResultCode'confidentialityRequired      ,13)
+          , (ResultCode'saslBindInProgress           ,14)
+          , (ResultCode'noSuchAttribute              ,16)
+          , (ResultCode'undefinedAttributeType       ,17)
+          , (ResultCode'inappropriateMatching        ,18)
+          , (ResultCode'constraintViolation          ,19)
+          , (ResultCode'attributeOrValueExists       ,20)
+          , (ResultCode'invalidAttributeSyntax       ,21)
+             -- 22-31 unused --
+          , (ResultCode'noSuchObject                 ,32)
+          , (ResultCode'aliasProblem                 ,33)
+          , (ResultCode'invalidDNSyntax              ,34)
+             -- 35 reserved for undefined isLeaf --
+          , (ResultCode'aliasDereferencingProblem    ,36)
+             -- 37-47 unused --
+          , (ResultCode'inappropriateAuthentication  ,48)
+          , (ResultCode'invalidCredentials           ,49)
+          , (ResultCode'insufficientAccessRights     ,50)
+          , (ResultCode'busy                         ,51)
+          , (ResultCode'unavailable                  ,52)
+          , (ResultCode'unwillingToPerform           ,53)
+          , (ResultCode'loopDetect                   ,54)
+             -- 55-63 unused --
+          , (ResultCode'namingViolation              ,64)
+          , (ResultCode'objectClassViolation         ,65)
+          , (ResultCode'notAllowedOnNonLeaf          ,66)
+          , (ResultCode'notAllowedOnRDN              ,67)
+          , (ResultCode'entryAlreadyExists           ,68)
+          , (ResultCode'objectClassModsProhibited    ,69)
+             -- 70 reserved for CLDAP --
+          , (ResultCode'affectsMultipleDSAs          ,71)
+             -- 72-79 unused --
+          , (ResultCode'other                        ,80)
+          ]
+
+-- | 'LDAPv3.LDAPResult' Result Code
+data ResultCode
+    = ResultCode'success
+    | ResultCode'operationsError
+    | ResultCode'protocolError
+    | ResultCode'timeLimitExceeded
+    | ResultCode'sizeLimitExceeded
+    | ResultCode'compareFalse
+    | ResultCode'compareTrue
+    | ResultCode'authMethodNotSupported
+    | ResultCode'strongerAuthRequired
+      -- 9 reserved --
+    | ResultCode'referral
+    | ResultCode'adminLimitExceeded
+    | ResultCode'unavailableCriticalExtension
+    | ResultCode'confidentialityRequired
+    | ResultCode'saslBindInProgress
+    | ResultCode'noSuchAttribute
+    | ResultCode'undefinedAttributeType
+    | ResultCode'inappropriateMatching
+    | ResultCode'constraintViolation
+    | ResultCode'attributeOrValueExists
+    | ResultCode'invalidAttributeSyntax
+      -- 22-31 unused --
+    | ResultCode'noSuchObject
+    | ResultCode'aliasProblem
+    | ResultCode'invalidDNSyntax
+      -- 35 reserved for undefined isLeaf --
+    | ResultCode'aliasDereferencingProblem
+      -- 37-47 unused --
+    | ResultCode'inappropriateAuthentication
+    | ResultCode'invalidCredentials
+    | ResultCode'insufficientAccessRights
+    | ResultCode'busy
+    | ResultCode'unavailable
+    | ResultCode'unwillingToPerform
+    | ResultCode'loopDetect
+      -- 55-63 unused --
+    | ResultCode'namingViolation
+    | ResultCode'objectClassViolation
+    | ResultCode'notAllowedOnNonLeaf
+    | ResultCode'notAllowedOnRDN
+    | ResultCode'entryAlreadyExists
+    | ResultCode'objectClassModsProhibited
+      -- 70 reserved for CLDAP --
+    | ResultCode'affectsMultipleDSAs
+      -- 72-79 unused --
+    | ResultCode'other
+    deriving (Generic,Show,Eq,Ord,Bounded,Enum)
+
+instance NFData ResultCode where
+  rnf = rwhnf
+
+instance ASN1 ResultCode where
+  asn1decode = (\(ENUMERATED x) -> x) <$> asn1decode
+  asn1encode = asn1encode . ENUMERATED
+
+instance Enumerated ResultCode where
+  toEnumerated   = Map.lookup `flip` toEnumeratedMap
+  fromEnumerated = Map.findWithDefault undefined `flip` fromEnumeratedMap
diff --git a/test/Arbitrary.hs b/test/Arbitrary.hs
new file mode 100644
--- /dev/null
+++ b/test/Arbitrary.hs
@@ -0,0 +1,234 @@
+-- Copyright (c) 2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE FlexibleInstances #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Arbitrary () where
+
+import           LDAPv3
+
+import           Data.Coerce               (coerce)
+import           Data.Int
+import qualified Data.Text.Short           as TS
+import           Test.QuickCheck.Instances ()
+import           Test.Tasty.QuickCheck
+
+instance Arbitrary TS.ShortText where
+  arbitrary = TS.fromText <$> arbitrary
+  shrink t = map TS.fromText (shrink (TS.toText t))
+
+instance Arbitrary x => Arbitrary (IMPLICIT tag x) where
+  arbitrary = IMPLICIT <$> arbitrary
+  shrink (IMPLICIT x) = coerce (shrink x)
+
+instance Arbitrary x => Arbitrary (EXPLICIT tag x) where
+  arbitrary = EXPLICIT <$> arbitrary
+  shrink (EXPLICIT x) = coerce (shrink x)
+
+instance Arbitrary x => Arbitrary (SET x) where
+  arbitrary = SET <$> arbitrary
+  shrink (SET x) = coerce (shrink x)
+
+instance Arbitrary x => Arbitrary (SET1 x) where
+  arbitrary = SET1 <$> arbitrary
+  shrink (SET1 x) = coerce (shrink x)
+
+instance Arbitrary MessageID where
+  arbitrary = MessageID <$> arbitrary
+  shrink (MessageID i) = coerce (shrink i)
+
+instance Arbitrary (UInt 1 127 Int8) where
+  arbitrary = either (\_ -> 1) id . toUInt <$> choose (1,127)
+
+instance Arbitrary (UInt 0 MaxInt Int32) where
+  arbitrary = int2msgid <$> arbitrary
+  shrink = map int2msgid . shrink . fromUInt
+
+int2msgid :: Int32 -> UInt 0 MaxInt Int32
+int2msgid = either (\_ -> 0) id . toUInt . abs
+
+instance Arbitrary ResultCode where
+  arbitrary = arbitraryBoundedEnum
+  shrink ResultCode'success = []
+  shrink _                  = [ResultCode'success]
+
+instance Arbitrary LDAPMessage where
+  arbitrary = LDAPMessage <$> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary BOOLEAN_DEFAULT_FALSE where
+  arbitrary = pure BOOL_TRUE
+
+instance Arbitrary Control where
+  arbitrary = Control <$> arbitrary <*> oneof [pure Nothing, pure (Just BOOL_TRUE)] <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary LDAPResult where
+  arbitrary = LDAPResult <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary ProtocolOp where
+  arbitrary = frequency
+    [ (2, ProtocolOp'bindRequest   <$> arbitrary)
+    , (2, ProtocolOp'bindResponse    <$> arbitrary)
+    , (1, ProtocolOp'unbindRequest  <$> arbitrary)
+    , (5, ProtocolOp'searchRequest  <$> arbitrary)
+    , (1, ProtocolOp'searchResDone  <$> arbitrary)
+    , (5, ProtocolOp'searchResEntry <$> arbitrary)
+    , (2, ProtocolOp'searchResRef   <$> arbitrary)
+    , (2, ProtocolOp'modifyRequest  <$> arbitrary)
+    , (1, ProtocolOp'modifyResponse <$> arbitrary)
+    , (2, ProtocolOp'addRequest     <$> arbitrary)
+    , (1, ProtocolOp'addResponse    <$> arbitrary)
+    , (1, ProtocolOp'delRequest     <$> arbitrary)
+    , (1, ProtocolOp'delResponse    <$> arbitrary)
+    , (2, ProtocolOp'modDNRequest   <$> arbitrary)
+    , (1, ProtocolOp'modDNResponse  <$> arbitrary)
+    , (2, ProtocolOp'compareRequest <$> arbitrary)
+    , (1, ProtocolOp'compareResponse <$> arbitrary)
+    , (1, ProtocolOp'abandonRequest <$> arbitrary)
+    , (2, ProtocolOp'extendedReq    <$> arbitrary)
+    , (2, ProtocolOp'extendedResp   <$> arbitrary)
+    , (2, ProtocolOp'intermediateResponse <$> arbitrary)
+    ]
+
+  shrink = genericShrink
+
+instance Arbitrary BindRequest where
+  arbitrary = BindRequest <$> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary AuthenticationChoice where
+  arbitrary = oneof [ AuthenticationChoice'simple <$> arbitrary
+                    , AuthenticationChoice'sasl <$> arbitrary
+                    ]
+  shrink = genericShrink
+
+instance Arbitrary SaslCredentials where
+  arbitrary = SaslCredentials <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary BindResponse where
+  arbitrary = BindResponse <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary SearchRequest where
+  arbitrary = SearchRequest <$> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+                            <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary Scope where
+  arbitrary = arbitraryBoundedEnum
+  shrink = genericShrink
+
+instance Arbitrary DerefAliases where
+  arbitrary = arbitraryBoundedEnum
+  shrink = genericShrink
+
+instance Arbitrary Filter where
+  arbitrary = frequency
+    [(  1, Filter'and <$> arbitrary)
+    ,(  1, Filter'or <$> arbitrary)
+    ,(  1, Filter'not <$> arbitrary)
+    ,(100, Filter'equalityMatch <$> arbitrary)
+    ,(100, Filter'substrings <$> arbitrary)
+    ,(100, Filter'greaterOrEqual <$> arbitrary)
+    ,(100, Filter'lessOrEqual <$> arbitrary)
+    ,(100, Filter'present <$> arbitrary)
+    ,(100, Filter'approxMatch <$> arbitrary)
+    ,(100, Filter'extensibleMatch <$> arbitrary)
+    ]
+  shrink = genericShrink
+
+instance Arbitrary SubstringFilter where
+  arbitrary = SubstringFilter <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary MatchingRuleAssertion where
+  arbitrary = MatchingRuleAssertion <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary Substring where
+  arbitrary = oneof [ Substring'initial <$> arbitrary
+                    , Substring'any <$> arbitrary
+                    , Substring'final <$> arbitrary
+                    ]
+  shrink = genericShrink
+
+instance Arbitrary SearchResultEntry where
+  arbitrary = SearchResultEntry <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary PartialAttribute where
+  arbitrary = PartialAttribute <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary Attribute where
+  arbitrary = Attribute <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary SearchResultReference where
+  arbitrary = SearchResultReference <$> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary ModifyRequest where
+  arbitrary = ModifyRequest <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary Change where
+  arbitrary = Change <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary Operation where
+  arbitrary = arbitraryBoundedEnum
+  shrink = genericShrink
+
+instance Arbitrary AddRequest where
+  arbitrary = AddRequest <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary CompareRequest where
+  arbitrary = CompareRequest <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary AttributeValueAssertion where
+  arbitrary = AttributeValueAssertion <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary ModifyDNRequest where
+  arbitrary = ModifyDNRequest <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary ExtendedRequest where
+  arbitrary = ExtendedRequest <$> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary ExtendedResponse where
+  arbitrary = ExtendedResponse <$> arbitrary <*> arbitrary <*> arbitrary
+  shrink = genericShrink
+
+instance Arbitrary IntermediateResponse where
+  arbitrary = IntermediateResponse <$> arbitrary <*> arbitrary
+  shrink = genericShrink
diff --git a/test/main.hs b/test/main.hs
new file mode 100644
--- /dev/null
+++ b/test/main.hs
@@ -0,0 +1,455 @@
+-- Copyright (c) 2019  Herbert Valerio Riedel <hvr@gnu.org>
+--
+--  This file is free software: you may copy, redistribute and/or modify it
+--  under the terms of the GNU General Public License as published by the
+--  Free Software Foundation, either version 2 of the License, or (at your
+--  option) any later version.
+--
+--  This file is distributed in the hope that it will be useful, but
+--  WITHOUT ANY WARRANTY; without even the implied warranty of
+--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+--  General Public License for more details.
+--
+--  You should have received a copy of the GNU General Public License
+--  along with this program (see `LICENSE`).  If not, see
+--  <https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.
+
+{-# LANGUAGE OverloadedLists     #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main (main) where
+
+import           LDAPv3
+
+import qualified Codec.Base16          as B16
+import           Data.Binary           as Bin
+import qualified Data.ByteString.Lazy  as BSL
+import           Data.Char             (isSpace)
+import           Data.Either
+import           Data.List.NonEmpty    (NonEmpty (..))
+import qualified Data.Text.Short       as TS
+
+import           Test.Tasty
+import           Test.Tasty.HUnit
+import           Test.Tasty.QuickCheck as QC
+
+import           Arbitrary             ()
+
+main :: IO ()
+main = defaultMain tests
+
+tests, qcProps, unitTests :: TestTree
+tests = testGroup "Tests" [unitTests, qcProps]
+
+----------------------------------------------------------------------------
+
+qcProps = testGroup "Properties"
+  [ QC.testProperty "MessageID round-trip" $
+      \msgid -> let msg = LDAPMessage msgid
+                                      (ProtocolOp'bindRequest (BindRequest 3 "" (AuthenticationChoice'simple "")))
+                                      Nothing
+                in decode (encode msg) == msg
+
+  , QC.testProperty "decode . encode == id @LDAPMessage" $
+      \(msg :: LDAPMessage) -> (decode . encode) msg === msg
+
+  , QC.testProperty "decode multiple" $
+      \msg1 msg2 -> decodeMulti (encode msg1 `mappend` encode msg2) === ([msg1,msg2],mempty)
+
+  , QC.testProperty "decode with noise" $
+      \msg1 noise -> decodeOne (encode msg1 `mappend` noise) === Right (msg1,noise)
+
+  , QC.testProperty "decode garbage" $ -- this has a very low probability of the random noise being a valid LDAPMessage
+      \noise -> isLeft (decodeOne noise)
+
+  ]
+
+decodeOne :: BSL.ByteString -> Either BSL.ByteString (LDAPMessage,BSL.ByteString)
+decodeOne raw = case decodeOrFail raw of
+  Left (rest,_,_)  -> Left rest
+  Right (rest,_,v) -> Right (v,rest)
+
+decodeMulti :: BSL.ByteString -> ([LDAPMessage],BSL.ByteString)
+decodeMulti = go []
+  where
+    go acc raw
+      | BSL.null raw = (reverse acc, raw)
+      | otherwise = case decodeOne raw of
+          Left rest      -> (reverse acc, rest)
+          Right (v,rest) -> go (v:acc) rest
+
+----------------------------------------------------------------------------
+
+-- local helper
+hex :: TS.ShortText -> BSL.ByteString
+hex = either error id . B16.decode . TS.toShortByteString . TS.filter (not . isSpace)
+
+unitTests = testGroup "Reference samples"
+  [ testGroup tlabel
+    [ testCase "encode" $ Bin.encode ref_msg @?= ref_bin
+    , testCase "decode" $ Bin.decode ref_bin @?= ref_msg
+    ]
+
+  | (tlabel,ref_bin,ref_msg) <-
+
+    [ ( "bindRequest #1"
+      , hex"30 0c 02 01 01 60 07 02 01 03 04 00 80 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 1
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindRequest
+                        ( BindRequest
+                            { bindRequest'version = 3
+                            , bindRequest'name = ""
+                            , bindRequest'authentication = AuthenticationChoice'simple ""
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "bindRequest #2"
+      , hex"30 2b 02 01 01 60 26 02 01 03 04 1a 63 6e 3d 61 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 80 05 61 64 6d 69 6e"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 1
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindRequest
+                        ( BindRequest
+                            { bindRequest'version = 3
+                            , bindRequest'name = "cn=admin,dc=example,dc=org"
+                            , bindRequest'authentication = AuthenticationChoice'simple "admin"
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "bindRequest #3"
+      , hex"30 18 02 01 02 60 13 02 01 03 04 00 a3 0c 04 0a 44 49 47 45 53 54 2d 4d 44 35"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindRequest
+                        ( BindRequest
+                            { bindRequest'version = 3
+                            , bindRequest'name = ""
+                            , bindRequest'authentication = AuthenticationChoice'sasl
+                                ( IMPLICIT
+                                    ( SaslCredentials
+                                        { _SaslCredentials'mechanism = "DIGEST-MD5"
+                                        , _SaslCredentials'credentials = Nothing
+                                        }
+                                    )
+                                )
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "bindResponse #1"
+      , hex"30 0c 02 01 01 61 07 0a 01 00 04 00 04 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 1
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindResponse
+                        ( BindResponse
+                            { _BindResponse'LDAPResult = LDAPResult
+                                { _LDAPResult'resultCode = ResultCode'success
+                                , _LDAPResult'matchedDN = ""
+                                , _LDAPResult'diagnosticMessage = ""
+                                , _LDAPResult'referral = Nothing
+                                }
+                            , _BindResponse'serverSaslCreds = Nothing
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "bindResponse #2"
+      , hex"30 41 02 01 01 61 3c 0a 01 35 04 00 04 35 75 6e 61 75 74 68 65 6e 74 69 63 61 74 65 64 20 62 69 6e 64 20 28 44 4e 20 77 69 74 68 20 6e 6f 20 70 61 73 73 77 6f 72 64 29 20 64 69 73 61 6c 6c 6f 77 65 64"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 1
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindResponse
+                        ( BindResponse
+                            { _BindResponse'LDAPResult = LDAPResult
+                                { _LDAPResult'resultCode = ResultCode'unwillingToPerform
+                                , _LDAPResult'matchedDN = ""
+                                , _LDAPResult'diagnosticMessage = "unauthenticated bind (DN with no password) disallowed"
+                                , _LDAPResult'referral = Nothing
+                                }
+                            , _BindResponse'serverSaslCreds = Nothing
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "bindResponse #3"
+      , hex"30 0c 02 01 01 61 07 0a 01 31 04 00 04 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 1
+                    , _LDAPMessage'protocolOp = ProtocolOp'bindResponse
+                        ( BindResponse
+                            { _BindResponse'LDAPResult = LDAPResult
+                                { _LDAPResult'resultCode = ResultCode'invalidCredentials
+                                , _LDAPResult'matchedDN = ""
+                                , _LDAPResult'diagnosticMessage = ""
+                                , _LDAPResult'referral = Nothing
+                                }
+                            , _BindResponse'serverSaslCreds = Nothing
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+
+    , ( "searchRequest #1"
+      , hex"30 36 02 01 02 63 31 04 11 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 0a 01 02 0a 01 00 02 01 00 02 01 00 01 01 00 87 0b 6f 62 6a 65 63 74 63 6c 61 73 73 30 00"
+
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchRequest
+                        ( SearchRequest
+                            { _SearchRequest'baseObject = "dc=example,dc=org"
+                            , _SearchRequest'scope = Scope'wholeSubtree
+                            , _SearchRequest'derefAliases = DerefAliases'neverDerefAliases
+                            , _SearchRequest'sizeLimit = 0
+                            , _SearchRequest'timeLimit = 0
+                            , _SearchRequest'typesOnly = False
+                            , _SearchRequest'filter = Filter'present ( IMPLICIT "objectclass" )
+                            , _SearchRequest'attributes = []
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchRequest #2"
+      , hex"30 42 02 01 02 63 3d 04 11 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 0a 01 02 0a 01 00 02 01 00 02 01 00 01 01 00 a2 17 a3 15 04 0b 6f 62 6a 65 63 74 43 6c 61 73 73 04 06 70 65 72 73 6f 6e 30 00"
+
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchRequest
+                        ( SearchRequest
+                            { _SearchRequest'baseObject = "dc=example,dc=org"
+                            , _SearchRequest'scope = Scope'wholeSubtree
+                            , _SearchRequest'derefAliases = DerefAliases'neverDerefAliases
+                            , _SearchRequest'sizeLimit = 0
+                            , _SearchRequest'timeLimit = 0
+                            , _SearchRequest'typesOnly = False
+                            , _SearchRequest'filter = Filter'not
+                                ( EXPLICIT
+                                    ( Filter'equalityMatch
+                                        ( IMPLICIT
+                                            ( AttributeValueAssertion
+                                                { _AttributeValueAssertion'attributeDesc = "objectClass"
+                                                , _AttributeValueAssertion'assertionValue = "person"
+                                                }
+                                            )
+                                        )
+                                    )
+                                )
+                            , _SearchRequest'attributes = []
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchRequest #3"
+      , hex"30 81 80 02 01 02 63 7b 04 11 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 0a 01 02 0a 01 00 02 01 00 02 01 00 01 01 00 a0 55 a2 3c a1 3a a9 1f 82 02 6f 75 83 16 52 65 73 65 61 72 63 68 41 6e 64 44 65 76 65 6c 6f 70 6d 65 6e 74 84 01 ff a9 17 82 02 6f 75 83 0e 48 75 6d 61 6e 52 65 73 6f 75 72 63 65 73 84 01 ff a3 15 04 0b 6f 62 6a 65 63 74 43 6c 61 73 73 04 06 70 65 72 73 6f 6e 30 00"
+
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchRequest
+                        ( SearchRequest
+                            { _SearchRequest'baseObject = "dc=example,dc=org"
+                            , _SearchRequest'scope = Scope'wholeSubtree
+                            , _SearchRequest'derefAliases = DerefAliases'neverDerefAliases
+                            , _SearchRequest'sizeLimit = 0
+                            , _SearchRequest'timeLimit = 0
+                            , _SearchRequest'typesOnly = False
+                            , _SearchRequest'filter = Filter'and
+                                ( IMPLICIT
+                                    ( SET1
+                                        ( Filter'not
+                                            ( EXPLICIT
+                                                ( Filter'or
+                                                    ( IMPLICIT
+                                                        ( SET1
+                                                            ( Filter'extensibleMatch
+                                                                ( IMPLICIT
+                                                                    ( MatchingRuleAssertion
+                                                                        { _MatchingRuleAssertion'matchingRule = Nothing
+                                                                        , _MatchingRuleAssertion'type = Just ( IMPLICIT "ou" )
+                                                                        , _MatchingRuleAssertion'matchValue = IMPLICIT "ResearchAndDevelopment"
+                                                                        , _MatchingRuleAssertion'dnAttributes = Just ( IMPLICIT BOOL_TRUE )
+                                                                        }
+                                                                    )
+                                                                ) :|
+                                                                [ Filter'extensibleMatch
+                                                                    ( IMPLICIT
+                                                                        ( MatchingRuleAssertion
+                                                                            { _MatchingRuleAssertion'matchingRule = Nothing
+                                                                            , _MatchingRuleAssertion'type = Just ( IMPLICIT "ou" )
+                                                                            , _MatchingRuleAssertion'matchValue = IMPLICIT "HumanResources"
+                                                                            , _MatchingRuleAssertion'dnAttributes = Just ( IMPLICIT BOOL_TRUE )
+                                                                            }
+                                                                        )
+                                                                    )
+                                                                ]
+                                                            )
+                                                        )
+                                                    )
+                                                )
+                                            ) :|
+                                            [ Filter'equalityMatch
+                                                ( IMPLICIT
+                                                    ( AttributeValueAssertion
+                                                        { _AttributeValueAssertion'attributeDesc = "objectClass"
+                                                        , _AttributeValueAssertion'assertionValue = "person"
+                                                        }
+                                                    )
+                                                )
+                                            ]
+                                        )
+                                    )
+                                )
+                            , _SearchRequest'attributes = []
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+
+
+      )
+
+    , ( "searchRequest #4"
+      , hex"30 3a 02 01 02 63 35 04 11 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 0a 01 02 0a 01 00 02 01 00 02 01 00 01 01 00 a4 0f 04 02 63 6e 30 09 80 01 61 81 01 6d 82 01 6e 30 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchRequest
+                        ( SearchRequest
+                            { _SearchRequest'baseObject = "dc=example,dc=org"
+                            , _SearchRequest'scope = Scope'wholeSubtree
+                            , _SearchRequest'derefAliases = DerefAliases'neverDerefAliases
+                            , _SearchRequest'sizeLimit = 0
+                            , _SearchRequest'timeLimit = 0
+                            , _SearchRequest'typesOnly = False
+                            , _SearchRequest'filter = Filter'substrings
+                                ( IMPLICIT
+                                    ( SubstringFilter
+                                        { _SubstringFilter'type = "cn"
+                                        , _SubstringFilter'substrings =
+                                            [ Substring'initial "a"
+                                            , Substring'any "m"
+                                            , Substring'final "n"
+                                            ]
+                                        }
+                                    )
+                                )
+                            , _SearchRequest'attributes = []
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchResEntry #1"
+      , hex"30 6e 02 01 02 64 69 04 11 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 30 54 30 2c 04 0b 6f 62 6a 65 63 74 43 6c 61 73 73 31 1d 04 03 74 6f 70 04 08 64 63 4f 62 6a 65 63 74 04 0c 6f 72 67 61 6e 69 7a 61 74 69 6f 6e 30 13 04 01 6f 31 0e 04 0c 45 78 61 6d 70 6c 65 20 49 6e 63 2e 30 0f 04 02 64 63 31 09 04 07 65 78 61 6d 70 6c 65"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchResEntry
+                        ( SearchResultEntry
+                            { _SearchResultEntry'objectName = "dc=example,dc=org"
+                            , _SearchResultEntry'attributes =
+                                [ PartialAttribute
+                                    { _PartialAttribute'type = "objectClass"
+                                    , _PartialAttribute'vals = SET
+                                        [ "top"
+                                        , "dcObject"
+                                        , "organization"
+                                        ]
+                                    }
+                                , PartialAttribute
+                                    { _PartialAttribute'type = "o"
+                                    , _PartialAttribute'vals = SET [ "Example Inc." ]
+                                    }
+                                , PartialAttribute
+                                    { _PartialAttribute'type = "dc"
+                                    , _PartialAttribute'vals = SET [ "example" ]
+                                    }
+                                ]
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchResEntry #2"
+      , hex"30 81 ce 02 01 02 64 81 c8 04 1a 63 6e 3d 61 64 6d 69 6e 2c 64 63 3d 65 78 61 6d 70 6c 65 2c 64 63 3d 6f 72 67 30 81 a9 30 39 04 0b 6f 62 6a 65 63 74 43 6c 61 73 73 31 2a 04 14 73 69 6d 70 6c 65 53 65 63 75 72 69 74 79 4f 62 6a 65 63 74 04 12 6f 72 67 61 6e 69 7a 61 74 69 6f 6e 61 6c 52 6f 6c 65 30 0d 04 02 63 6e 31 07 04 05 61 64 6d 69 6e 30 23 04 0b 64 65 73 63 72 69 70 74 69 6f 6e 31 14 04 12 4c 44 41 50 20 61 64 6d 69 6e 69 73 74 72 61 74 6f 72 30 38 04 0c 75 73 65 72 50 61 73 73 77 6f 72 64 31 28 04 26 7b 53 53 48 41 7d 54 66 50 53 6f 37 46 68 58 38 63 34 53 6b 6c 4f 52 58 75 46 54 55 75 67 39 38 64 4e 46 4c 6b 34"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchResEntry
+                        ( SearchResultEntry
+                            { _SearchResultEntry'objectName = "cn=admin,dc=example,dc=org"
+                            , _SearchResultEntry'attributes =
+                                [ PartialAttribute
+                                    { _PartialAttribute'type = "objectClass"
+                                    , _PartialAttribute'vals = SET
+                                        [ "simpleSecurityObject"
+                                        , "organizationalRole"
+                                        ]
+                                    }
+                                , PartialAttribute
+                                    { _PartialAttribute'type = "cn"
+                                    , _PartialAttribute'vals = SET [ "admin" ]
+                                    }
+                                , PartialAttribute
+                                    { _PartialAttribute'type = "description"
+                                    , _PartialAttribute'vals = SET [ "LDAP administrator" ]
+                                    }
+                                , PartialAttribute
+                                    { _PartialAttribute'type = "userPassword"
+                                    , _PartialAttribute'vals = SET [ "{SSHA}TfPSo7FhX8c4SklORXuFTUug98dNFLk4" ]
+                                    }
+                                ]
+                            }
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchResDone #1"
+      , hex"30 0c 02 01 02 65 07 0a 01 00 04 00 04 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchResDone
+                        ( IMPLICIT
+                            ( LDAPResult
+                                { _LDAPResult'resultCode = ResultCode'success
+                                , _LDAPResult'matchedDN = ""
+                                , _LDAPResult'diagnosticMessage = ""
+                                , _LDAPResult'referral = Nothing
+                                }
+                            )
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "searchResDone #2"
+      , hex"30 0c 02 01 02 65 07 0a 01 20 04 00 04 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 2
+                    , _LDAPMessage'protocolOp = ProtocolOp'searchResDone
+                        ( IMPLICIT
+                            ( LDAPResult
+                                { _LDAPResult'resultCode = ResultCode'noSuchObject
+                                , _LDAPResult'matchedDN = ""
+                                , _LDAPResult'diagnosticMessage = ""
+                                , _LDAPResult'referral = Nothing
+                                }
+                            )
+                        )
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    , ( "unbindRequest"
+      , hex"30 05 02 01 03 42 00"
+      , LDAPMessage { _LDAPMessage'messageID = MessageID 3
+                    , _LDAPMessage'protocolOp = ProtocolOp'unbindRequest (IMPLICIT ())
+                    , _LDAPMessage'controls = Nothing
+                    }
+      )
+
+    ]
+
+  ]
