diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for cryptostore
+
+## 0.1.0.0 -- 2018-09-23
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2018, Olivier Chéron
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Olivier Chéron nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,269 @@
+# cryptostore
+
+[![Build Status](https://travis-ci.org/ocheron/cryptostore.png?branch=master)](https://travis-ci.org/ocheron/cryptostore)
+[![BSD](https://b.repl.ca/v1/license-BSD-blue.png)](https://en.wikipedia.org/wiki/BSD_licenses)
+[![Haskell](https://b.repl.ca/v1/language-haskell-lightgrey.png)](https://haskell.org/)
+
+This package allows to read and write cryptographic objects to/from ASN.1.
+
+Currently the following is implemented:
+
+* Reading and writing private keys with optional encryption (this extends
+  [x509-store](https://hackage.haskell.org/package/x509-store) API)
+
+* Reading and writing public keys, certificates and CRLs
+
+* PKCS #12 container format (password-based only)
+
+* Many parts of Cryptographic Message Syntax
+
+Please have a look at the examples below as well as some warnings about
+cryptographic algorithms.
+
+## Private Keys
+
+The API to read and write private keys is available in module
+`Crypto.Store.PKCS8`.  When encrypting, some types and functions from module
+`Crypto.Store.PKCS5` are also necessary.
+
+Reading a private key from disk:
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.Store.PKCS8
+> (key : _) <- readKeyFile "/path/to/privkey.pem" -- assuming single key
+> recover "mypassword" key
+Right (PrivKeyRSA ...)
+```
+
+Generating a private key and writing to disk, without encryption:
+
+```haskell
+> :m Crypto.PubKey.RSA Crypto.Store.PKCS8 Data.X509
+> privKey <- PrivKeyRSA . snd <$> generate (2048 `div` 8) 0x10001
+> writeKeyFile PKCS8Format "/path/to/privkey.pem" [privKey]
+```
+
+Generating a private key and writing to disk, with password-based encryption:
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.PubKey.RSA Crypto.Store.PKCS8 Data.X509 Crypto.Store.PKCS5
+> privKey <- PrivKeyRSA . snd <$> generate (2048 `div` 8) 0x10001
+> salt <- generateSalt 8
+> let kdf = PBKDF2 salt 2048 Nothing PBKDF2_SHA256
+> encParams <- generateEncryptionParams (CBC AES256)
+> let pbes = PBES2 (PBES2Parameter kdf encParams)
+> writeEncryptedKeyFile "/path/to/privkey.pem" pbes "mypassword" privKey
+Right ()
+```
+
+Parameters used in this example are AES-256-CBC as cipher, PBKDF2 as
+key-derivation function, with an 8-byte salt, 2048 iterations and SHA-256 as
+pseudorandom function.
+
+## Public Keys and Signed Objects
+
+Module `Crypto.Store.X509` provides functions to read/write PEM files containing
+public keys, X.509 certificates and CRLs.  These files are never encrypted.
+
+Reading a public key and certificate from disk:
+
+```haskell
+> :m Data.X509 Crypto.Store.X509
+> readPubKeyFile "/path/to/pubkey.pem"
+[PubKeyRSA ...]
+> readSignedObject "/path/to/cert.pem" :: IO [SignedCertificate]
+[SignedExact ...]
+```
+
+Writing back to disk:
+
+```haskell
+> :m Crypto.Store.X509
+> writePubKeyFile "/path/to/pubkey.pem" [pubKey]
+> writeSignedObject "/path/to/cert.pem" [cert]
+```
+
+## PKCS #12
+
+PKCS #12 is a complex format with multiple layers of protection, providing
+usually both privacy and integrity, with a single password for all or not.  The
+API to read PKCS #12 files requires some password at each layer.  This API is
+available in module `Crypto.Store.PKCS12`.
+
+Reading a binary PKCS #12 file using distinct integrity and privacy passwords:
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.Store.PKCS12
+> Right p12 <- readP12File "/path/to/file.p12"
+> let Right pkcs12 = recover "myintegrityassword" p12
+> let Right contents = recover "myprivacypassword" (unPKCS12 pkcs12)
+> getAllSafeX509Certs contents
+[SignedExact {getSigned = ...}]
+> recover "myprivacypassword" (getAllSafeKeys contents)
+Right [PrivKeyRSA ...]
+```
+
+Generating a PKCS #12 file containing a private key:
+
+```haskell
+> :set -XOverloadedStrings
+
+-- Generate a private key
+> :m Crypto.PubKey.RSA Data.X509
+> privKey <- PrivKeyRSA . snd <$> generate (2048 `div` 8) 0x10001
+
+-- Put the key inside a bag
+> :m Crypto.Store.PKCS12 Crypto.Store.PKCS8 Crypto.Store.PKCS5 Crypto.Store.CMS
+> let attrs = setFriendlyName "Some Key" []
+>     keyBag = Bag (KeyBag $ FormattedKey PKCS8Format privKey) attrs
+>     contents = SafeContents [keyBag]
+
+-- Encrypt the contents
+> salt <- generateSalt 8
+> let kdf = PBKDF2 salt 2048 Nothing PBKDF2_SHA256
+> encParams <- generateEncryptionParams (CBC AES256)
+> let pbes = PBES2 (PBES2Parameter kdf encParams)
+>     Right pkcs12 = encrypted pbes "mypassword" contents
+
+-- Save to PKCS #12 with integrity protection (same password)
+> salt' <- generateSalt 8
+> let iParams = (DigestAlgorithm SHA256, PBEParameter salt' 2048)
+> writeP12File "/path/to/privkey.p12" iParams "mypassword" pkcs12
+Right ()
+```
+
+The API also provides functions to generate/extract a pair containing a private
+key and a certificate chain.  This pair is the type alias `Credential` in `tls`.
+Currently the functions assume that the PKCS #12 file contains no other data
+than the credential.
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.Store.PKCS12 Crypto.Store.PKCS8 Crypto.Store.PKCS5 Crypto.Store.CMS
+
+-- Read PKCS #12 content as credential
+> Right p12 <- readP12File "/path/to/file.p12"
+> let Right pkcs12 = recover "myintegrityassword" p12
+> let Right (Just cred) = recover "myprivacypassword" (toCredential pkcs12)
+> cred
+(CertificateChain [...], PrivKeyRSA (...))
+
+-- Scheme to reencrypt the key
+> saltK <- generateSalt 8
+> let kdfK = PBKDF2 saltK 2048 Nothing PBKDF2_SHA256
+> encParamsK <- generateEncryptionParams (CBC AES256)
+> let sKey = PBES2 (PBES2Parameter kdfK encParamsK)
+
+-- Scheme to reencrypt the certificate chain
+> saltC <- generateSalt 8
+> let kdfC = PBKDF2 saltC 1024 Nothing PBKDF2_SHA256
+> encParamsC <- generateEncryptionParams (CBC AES128)
+> let sCert = PBES2 (PBES2Parameter kdfC encParamsC)
+
+-- Write the content back to a new file
+> let Right pkcs12' = fromCredential (Just sCert) sKey "myprivacypassword" cred
+> salt <- generateSalt 8
+> let iParams = (DigestAlgorithm SHA256, PBEParameter salt 2048)
+> writeP12File "/path/to/newfile.p12" iParams "myintegrityassword" pkcs12'
+```
+
+## Cryptographic Message Syntax
+
+The API to read and write CMS content is available in `Crypto.Store.CMS`.  The
+main data type `ContentInfo` represents a CMS structure.
+
+Implemented content types are:
+
+* data
+* signed data
+* enveloped data
+* digested data
+* encrypted data
+* authenticated data
+* and authenticated-enveloped data
+
+Notable omissions:
+
+* detached content
+* streaming
+* compressed data
+* and S/MIME external format (only PEM is supported, i.e. the textual encoding
+  of [RFC 7468](https://tools.ietf.org/html/rfc7468))
+
+The following examples generate a CMS structure enveloping some data to a
+password recipient, then decrypt the data to recover the content.
+
+### Generating enveloped data
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.Store.CMS
+
+-- Input content info
+> let info = DataCI "Hi, what will you need from the cryptostore?"
+
+-- Content encryption will use AES-128-CBC
+> ceParams <- generateEncryptionParams (CBC AES128)
+> ceKey <- generateKey ceParams :: IO ContentEncryptionKey
+
+-- Encrypt the Content Encryption Key with a Password Recipient Info,
+-- i.e. a KDF will derive the Key Encryption Key from a password
+-- that the recipient will need to know
+> salt <- generateSalt 8
+> let kdf = PBKDF2 salt 2048 Nothing PBKDF2_SHA256
+> keParams <- generateEncryptionParams (CBC AES128)
+> let pri = forPasswordRecipient "mypassword" kdf (PWRIKEK keParams)
+
+-- Generate the enveloped structure for this single recipient
+> Right envelopedCI <- envelopData mempty ceKey ceParams [pri] [] info
+> writeCMSFile "/path/to/enveloped.pem" [envelopedCI]
+```
+
+### Opening the enveloped data
+
+```haskell
+> :set -XOverloadedStrings
+> :m Crypto.Store.CMS
+
+-- Then this recipient just has to read the file and recover enveloped
+-- content using the password
+> [EnvelopedDataCI envelopedData] <- readCMSFile "/path/to/enveloped.pem"
+> openEnvelopedData (withRecipientPassword "mypassword") envelopedData
+Right (DataCI "Hi, what will you need from the cryptostore?")
+```
+
+## Algorithms and security
+
+For compatibility reasons cryptostore implements many outdated algorithms that
+are still in use in data formats.  Please check your security requirements.  New
+applications should favor PBKDF2 or Scrypt and AEAD ciphers.
+
+Additionally, the package is designed exclusively for store and forward
+scenarios, as most algorithms will not be perfectly safe for interactive use.
+ECDSA signature generation uses the generic ECC implementation from cryptonite
+and could leak the private key under timing attack.  A padding oracle on
+CBC-encrypted ciphertext allows to recover the plaintext.
+
+## Design
+
+Main dependencies are:
+
+* [cryptonite](https://hackage.haskell.org/package/cryptonite) implementation of
+  public-key systems, symmetric ciphers, KDFs, MAC, and one-way hash functions
+* [asn1-types](https://hackage.haskell.org/package/asn1-types) and
+  [asn1-encoding](https://hackage.haskell.org/package/asn1-encoding) to encode
+  and decode ASN.1 content
+* [pem](https://hackage.haskell.org/package/pem) to read and write PEM files
+* [x509](https://hackage.haskell.org/package/x509) contains the certificate and
+  private-key data types
+
+Internally the ASN.1 parser used is a local implementation extending the code of
+[asn1-parse](https://hackage.haskell.org/package/asn1-parse).  This extension is
+able to parse `ASN1Repr`, i.e. a stream of ASN.1 tags associated with the binary
+decoding events the tags were originated from.  Similarly generation of ASN.1
+content does not use the `ASN1S` type but an extension which is able to encode a
+stream where some parts have already been encoded.  Retaining the original
+BER/DER encoding is required when incorporating MACed or signed content.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cryptostore.cabal b/cryptostore.cabal
new file mode 100644
--- /dev/null
+++ b/cryptostore.cabal
@@ -0,0 +1,96 @@
+name:                cryptostore
+version:             0.1.0.0
+synopsis:            Serialization of cryptographic data types
+description:         Haskell implementation of PKCS \#8, PKCS \#12 and CMS
+                     (Cryptographic Message Syntax).
+license:             BSD3
+license-file:        LICENSE
+author:              Olivier Chéron
+maintainer:          olivier.cheron@gmail.com
+copyright:           Olivier Chéron
+category:            Cryptography, Codec
+homepage:            https://github.com/ocheron/cryptostore
+bug-reports:         https://github.com/ocheron/cryptostore/issues
+build-type:          Simple
+extra-source-files:  README.md ChangeLog.md tests/files/*.pem
+cabal-version:       >=1.10
+
+source-repository head
+  type:     git
+  location: https://github.com/ocheron/cryptostore
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Crypto.Store.CMS
+                     , Crypto.Store.Cipher.RC2
+                     , Crypto.Store.Error
+                     , Crypto.Store.PKCS5
+                     , Crypto.Store.KeyWrap.AES
+                     , Crypto.Store.KeyWrap.TripleDES
+                     , Crypto.Store.KeyWrap.RC2
+                     , Crypto.Store.PKCS12
+                     , Crypto.Store.PKCS8
+                     , Crypto.Store.X509
+  other-modules:       Crypto.Store.ASN1.Generate
+                     , Crypto.Store.ASN1.Parse
+                     , Crypto.Store.CMS.Algorithms
+                     , Crypto.Store.CMS.Attribute
+                     , Crypto.Store.CMS.AuthEnveloped
+                     , Crypto.Store.CMS.Encrypted
+                     , Crypto.Store.CMS.Enveloped
+                     , Crypto.Store.CMS.Info
+                     , Crypto.Store.CMS.OriginatorInfo
+                     , Crypto.Store.CMS.PEM
+                     , Crypto.Store.CMS.Signed
+                     , Crypto.Store.CMS.Type
+                     , Crypto.Store.CMS.Util
+                     , Crypto.Store.Cipher.RC2.Primitive
+                     , Crypto.Store.PEM
+                     , Crypto.Store.PKCS5.PBES1
+                     , Crypto.Store.PKCS8.EC
+                     , Crypto.Store.Util
+  -- other-extensions:
+  build-depends:       base >= 4.9 && < 5
+                     , bytestring
+                     , basement
+                     , memory
+                     , cryptonite >= 0.25
+                     , pem >= 0.1 && < 0.3
+                     , asn1-types >= 0.3.1 && < 0.4
+                     , asn1-encoding >= 0.9 && < 0.10
+                     , hourglass >= 0.2
+                     , x509 >= 1.7.3
+  default-language:    Haskell2010
+  ghc-options:         -Wall
+
+test-suite test-cryptostore
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      tests
+  main-is:             Main.hs
+  other-modules:       KeyWrap.AES
+                     , KeyWrap.TripleDES
+                     , KeyWrap.RC2
+                     , CMS.Instances
+                     , CMS.Tests
+                     , Cipher.RC2
+                     , PKCS12.Instances
+                     , PKCS12.Tests
+                     , PKCS8.Instances
+                     , PKCS8.Tests
+                     , Util
+                     , X509.Instances
+                     , X509.Tests
+  build-depends:       base >= 4.9 && < 5
+                     , bytestring
+                     , asn1-types >= 0.3.1 && < 0.4
+                     , cryptonite >= 0.25
+                     , memory
+                     , tasty
+                     , tasty-hunit
+                     , tasty-quickcheck
+                     , hourglass
+                     , pem
+                     , x509
+                     , cryptostore
+  default-language:    Haskell2010
+  ghc-options:         -Wall
diff --git a/src/Crypto/Store/ASN1/Generate.hs b/src/Crypto/Store/ASN1/Generate.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/ASN1/Generate.hs
@@ -0,0 +1,147 @@
+-- |
+-- Module      : Crypto.Store.ASN1.Generate
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Generating ASN.1
+module Crypto.Store.ASN1.Generate
+    ( ASN1Stream
+    , ASN1Elem()
+    , ASN1P()
+    , ASN1PS
+    , asn1Container
+    , gNull
+    , gIntVal
+    , gOID
+    , gASN1String
+    , gBMPString
+    , gOctetString
+    , gBitString
+    , gASN1Time
+    , gMany
+    , gEncoded
+    , optASN1S
+    , encodeASN1S
+    ) where
+
+import           Data.ASN1.BinaryEncoding
+import           Data.ASN1.BinaryEncoding.Raw
+import           Data.ASN1.BitArray
+import           Data.ASN1.Encoding
+import           Data.ASN1.OID
+import           Data.ASN1.Types
+import qualified Data.ByteArray as B
+import           Data.ByteString (ByteString)
+
+import Time.Types (DateTime, TimezoneOffset)
+
+-- | A stream of ASN.1 elements.
+type ASN1Stream e = [e] -> [e]
+
+-- | Elements in an ASN.1 stream.
+class ASN1Elem e where
+    -- | Create a container from an inner ASN.1 stream.
+    asn1Container :: ASN1ConstructionType -> ASN1Stream e -> ASN1Stream e
+    -- | Generate a list of ASN.1 elements.
+    gMany :: [ASN1] -> ASN1Stream e
+    -- | Generate one ASN.1 element.
+    gOne :: ASN1 -> ASN1Stream e
+
+instance ASN1Elem ASN1 where
+    asn1Container ty f = (Start ty :) . f . (End ty :)
+    gMany = (++)
+    gOne = (:)
+
+-- | Extend the 'ASN1' type to be able to encode to ASN.1 even when some parts
+-- of the stream have already been encoded.
+data ASN1P
+    = ASN1Prim [ASN1]
+      -- ^ Primitive elements (or constructed types that are fully terminated)
+    | ASN1Container !ASN1ConstructionType [ASN1P]
+      -- ^ Constructed type with inner structure kept
+    | ASN1Encoded !ByteString
+      -- ^ A part which has already been encoded
+
+instance ASN1Elem ASN1P where
+    asn1Container ty f = (ASN1Container ty (f []) :)
+    gMany asn1 = (ASN1Prim asn1 :)
+    gOne = gMany . (:[])
+
+-- | Prepend a list of 'ASN1P'.
+type ASN1PS = ASN1Stream ASN1P
+
+-- | Encode to ASN.1 a list of 'ASN1P' elements.  Outer encoding will be DER,
+-- but partially encoded inner 'ASN1Encoded' elements many have any encoding.
+pEncode :: [ASN1P] -> ByteString
+pEncode x = let (_, f) = run x in f B.empty
+  where
+    run []                   = (0, id)
+    run (ASN1Prim asn1 : as) = (B.length p + r, B.append p . ps)
+      where p       = encodeASN1' DER asn1
+            (r, ps) = run as
+    run (ASN1Encoded p : as) = (B.length p + r, B.append p . ps)
+      where (r, ps) = run as
+    run (ASN1Container ty children : as) =
+        (B.length header + l + r, B.append header . p . ps)
+      where (l, p)  = run children
+            (r, ps) = run as
+            header  = toByteString [Header $ ASN1Header cl tg True $ makeLen l]
+            (cl, tg) =
+                case ty of
+                    Container tyClass tyTag -> (tyClass, tyTag)
+                    Sequence -> (Universal, 0x10)
+                    Set -> (Universal, 0x11)
+
+    makeLen len
+        | len < 0x80 = LenShort len
+        | otherwise  = LenLong (nbBytes len) len
+    nbBytes nb = if nb > 255 then 1 + nbBytes (nb `div` 256) else 1
+
+-- | Generate a 'Null' ASN.1 element.
+gNull :: ASN1Elem e => ASN1Stream e
+gNull = gOne Null
+
+-- | Generate an 'IntVal' ASN.1 element.
+gIntVal :: ASN1Elem e => Integer -> ASN1Stream e
+gIntVal = gOne . IntVal
+
+-- | Generate an 'OID' ASN.1 element.
+gOID :: ASN1Elem e => OID -> ASN1Stream e
+gOID = gOne . OID
+
+-- | Generate an 'ASN1String' ASN.1 element.
+gASN1String :: ASN1Elem e => ASN1CharacterString -> ASN1Stream e
+gASN1String = gOne . ASN1String
+
+-- | Generate an @BMPString@ ASN.1 element.
+gBMPString :: ASN1Elem e => String -> ASN1Stream e
+gBMPString = gASN1String . asn1CharacterString BMP
+
+-- | Generate an 'OctetString' ASN.1 element.
+gOctetString :: ASN1Elem e => ByteString -> ASN1Stream e
+gOctetString = gOne . OctetString
+
+-- | Generate a 'BitString' ASN.1 element.
+gBitString :: ASN1Elem e => BitArray -> ASN1Stream e
+gBitString = gOne . BitString
+
+-- | Generate an 'ASN1Time' ASN.1 element.
+gASN1Time :: ASN1Elem e
+          => ASN1TimeType -> DateTime -> Maybe TimezoneOffset -> ASN1Stream e
+gASN1Time a b c = gOne (ASN1Time a b c)
+
+-- | Generate ASN.1 for an optional value.
+optASN1S :: Maybe a -> (a -> ASN1Stream e) -> ASN1Stream e
+optASN1S Nothing    _  = id
+optASN1S (Just val) fn = fn val
+
+-- | Generate ASN.1 for a part of the stream which is already encoded.
+gEncoded :: ByteString -> ASN1PS
+gEncoded = (:) . ASN1Encoded
+
+-- | Encode the ASN.1 stream to DER format (except for inner parts that are
+-- already encoded and may use another format).
+encodeASN1S :: ASN1PS -> ByteString
+encodeASN1S asn1 = pEncode (asn1 [])
diff --git a/src/Crypto/Store/ASN1/Parse.hs b/src/Crypto/Store/ASN1/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/ASN1/Parse.hs
@@ -0,0 +1,210 @@
+-- |
+-- Module      : Crypto.Store.ASN1.Parse
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Parser combinators for ASN.1 stream.  Similar to "Data.ASN1.Parse" but
+-- with the following additions:
+--
+-- * Parsed stream is annotated, i.e. parser input is @('ASN1', e)@ instead of
+--   @'ASN1'@.  Main motivation is to allow to parse a sequence of 'ASN1Repr'
+--   and hold the exact binary content that has been parsed.  As consequence,
+--   no @getObject@ function is provided.  Function 'withAnnotations' runs
+--   a parser and returns all annotations consumed in a monoid concatenation.
+--
+-- * The parser implements 'Alternative' and 'MonadPlus'.
+--
+-- * The 'fail' function returns a parse error so that pattern matching makes
+--   monadic parsing code easier to write.
+module Crypto.Store.ASN1.Parse
+    ( ParseASN1
+    -- * run
+    , runParseASN1State
+    , runParseASN1State_
+    , runParseASN1
+    , runParseASN1_
+    , throwParseError
+    -- * combinators
+    , onNextContainer
+    , onNextContainerMaybe
+    , getNextContainer
+    , getNextContainerMaybe
+    , getNext
+    , getNextMaybe
+    , hasNext
+    , getMany
+    , withAnnotations
+    ) where
+
+import Data.ASN1.Types
+import Data.Monoid
+import Control.Applicative
+import Control.Arrow (first)
+import Control.Monad (MonadPlus(..), liftM2)
+import Control.Monad.Fail
+
+data State e = State [(ASN1, e)] !e
+
+-- | ASN1 parse monad
+newtype ParseASN1 e a = P { runP :: State e -> Either String (a, State e) }
+
+instance Functor (ParseASN1 e) where
+    fmap f m = P (fmap (first f) . runP m)
+instance Applicative (ParseASN1 e) where
+    pure a = P $ \s -> Right (a, s)
+    (<*>) mf ma = P $ \s ->
+        case runP mf s of
+            Left err      -> Left err
+            Right (f, s2) ->
+                case runP ma s2 of
+                    Left err      -> Left err
+                    Right (a, s3) -> Right (f a, s3)
+instance Alternative (ParseASN1 e) where
+    empty = throwParseError "empty"
+    (<|>) = mplus
+instance Monad (ParseASN1 e) where
+    return      = pure
+    (>>=) m1 m2 = P $ \s ->
+        case runP m1 s of
+            Left err      -> Left err
+            Right (a, s2) -> runP (m2 a) s2
+    fail        = throwParseError
+instance MonadFail (ParseASN1 e) where
+    fail = throwParseError
+instance MonadPlus (ParseASN1 e) where
+    mzero = throwParseError "mzero"
+    mplus m1 m2 = P $ \s ->
+        case runP m1 s of
+            Left  _ -> runP m2 s
+            success -> success
+
+get :: ParseASN1 e (State e)
+get = P $ \stream -> Right (stream, stream)
+
+put :: State e -> ParseASN1 e ()
+put stream = P $ \_ -> Right ((), stream)
+
+-- | throw a parse error
+throwParseError :: String -> ParseASN1 e a
+throwParseError s = P $ \_ -> Left s
+
+wrap :: ASN1 -> (ASN1, ())
+wrap a = (a, ())
+
+unwrap :: (ASN1, ()) -> ASN1
+unwrap (a, ()) = a
+
+-- | run the parse monad over a stream and returns the result and the remaining ASN1 Stream.
+runParseASN1State :: ParseASN1 () a -> [ASN1] -> Either String (a, [ASN1])
+runParseASN1State f a = do
+    (a', list) <- runParseASN1State_ f (map wrap a)
+    return (a', map unwrap list)
+
+-- | run the parse monad over a stream and returns the result and the remaining ASN1 Stream.
+runParseASN1State_ :: Monoid e => ParseASN1 e a -> [(ASN1, e)] -> Either String (a, [(ASN1, e)])
+runParseASN1State_ f a = do
+    (r, State a' _) <- runP f (State a mempty)
+    return (r, a')
+
+-- | run the parse monad over a stream and returns the result.
+--
+-- If there's still some asn1 object in the state after calling f,
+-- an error will be raised.
+runParseASN1 :: ParseASN1 () a -> [ASN1] -> Either String a
+runParseASN1 f s = runParseASN1_ f (map wrap s)
+
+-- | run the parse monad over a stream and returns the result.
+--
+-- If there's still some asn1 object in the state after calling f,
+-- an error will be raised.
+runParseASN1_ :: Monoid e => ParseASN1 e a -> [(ASN1, e)] -> Either String a
+runParseASN1_ f s =
+    case runP f (State s mempty) of
+        Left err              -> Left err
+        Right (o, State [] _) -> Right o
+        Right (_, State er _) ->
+            Left ("runParseASN1_: remaining state " ++ show (map fst er))
+
+-- | get next element from the stream
+getNext :: Monoid e => ParseASN1 e ASN1
+getNext = do
+    list <- get
+    case list of
+        State []        _  -> throwParseError "empty"
+        State ((h,e):l) es -> put (State l (es <> e)) >> return h
+
+-- | get many elements until there's nothing left
+getMany :: ParseASN1 e a -> ParseASN1 e [a]
+getMany getOne = do
+    next <- hasNext
+    if next
+        then liftM2 (:) getOne (getMany getOne)
+        else return []
+
+-- | get next element from the stream maybe
+getNextMaybe :: Monoid e => (ASN1 -> Maybe a) -> ParseASN1 e (Maybe a)
+getNextMaybe f = do
+    list <- get
+    case list of
+        State []        _  -> return Nothing
+        State ((h,e):l) es -> let r = f h
+                               in do case r of
+                                         Nothing -> put list
+                                         Just _  -> put (State l (es <> e))
+                                     return r
+
+-- | get next container of specified type and return all its elements
+getNextContainer :: Monoid e => ASN1ConstructionType -> ParseASN1 e [(ASN1, e)]
+getNextContainer ty = do
+    list <- get
+    case list of
+        State []        _                  -> throwParseError "empty"
+        State ((h,e):l) es | h == Start ty -> do let (l1, l2) = getConstructedEnd 0 (State l (es <> e))
+                                                 put l2 >> return l1
+                           | otherwise     -> throwParseError "not an expected container"
+
+
+-- | run a function of the next elements of a container of specified type
+onNextContainer :: Monoid e => ASN1ConstructionType -> ParseASN1 e a -> ParseASN1 e a
+onNextContainer ty f = getNextContainer ty >>= either throwParseError return . runParseASN1_ f
+
+-- | just like getNextContainer, except it doesn't throw an error if the container doesn't exists.
+getNextContainerMaybe :: Monoid e => ASN1ConstructionType -> ParseASN1 e (Maybe [(ASN1, e)])
+getNextContainerMaybe ty = do
+    list <- get
+    case list of
+        State []        _                  -> return Nothing
+        State ((h,e):l) es | h == Start ty -> do let (l1, l2) = getConstructedEnd 0 (State l (es <> e))
+                                                 put l2 >> return (Just l1)
+                           | otherwise     -> return Nothing
+
+-- | just like onNextContainer, except it doesn't throw an error if the container doesn't exists.
+onNextContainerMaybe :: Monoid e => ASN1ConstructionType -> ParseASN1 e a -> ParseASN1 e (Maybe a)
+onNextContainerMaybe ty f = do
+    n <- getNextContainerMaybe ty
+    case n of
+        Just l  -> either throwParseError (return . Just) $ runParseASN1_ f l
+        Nothing -> return Nothing
+
+-- | returns if there's more elements in the stream.
+hasNext :: ParseASN1 e Bool
+hasNext = do State l _ <- get; return . not . null $ l
+
+-- | run a parser and return its result as well as all annotations that were used
+withAnnotations :: Monoid e => ParseASN1 e a -> ParseASN1 e (a, e)
+withAnnotations f = do
+    State l es <- get
+    case runP f (State l mempty) of
+        Left err                -> throwParseError err
+        Right (a, State l' es') -> do put (State l' (es <> es'))
+                                      return (a, es')
+
+getConstructedEnd :: Monoid e => Int -> State e -> ([(ASN1, e)], State e)
+getConstructedEnd _ xs@(State [] _)                = ([], xs)
+getConstructedEnd i (State (x@(Start _, e):xs) es) = let (yz, zs) = getConstructedEnd (i+1) (State xs (es <> e)) in (x:yz, zs)
+getConstructedEnd i (State (x@(End _, e):xs) es)
+    | i == 0    = ([], State xs (es <> e))
+    | otherwise = let (ys, zs) = getConstructedEnd (i-1) (State xs (es <> e)) in (x:ys, zs)
+getConstructedEnd i (State (x@(_, e):xs) es)       = let (ys, zs) = getConstructedEnd i (State xs (es <> e)) in (x:ys, zs)
diff --git a/src/Crypto/Store/CMS.hs b/src/Crypto/Store/CMS.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS.hs
@@ -0,0 +1,439 @@
+-- |
+-- Module      : Crypto.Store.CMS
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Cryptographic Message Syntax
+--
+-- * <https://tools.ietf.org/html/rfc5652 RFC 5652>: Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc3370 RFC 3370>: Cryptographic Message Syntax (CMS) Algorithms
+-- * <https://tools.ietf.org/html/rfc3560 RFC 3560>: Use of the RSAES-OAEP Key Transport Algorithm in the Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc4056 RFC 4056>: Use of the RSASSA-PSS Signature Algorithm in Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc3565 RFC 3565>: Use of the Advanced Encryption Standard (AES) Encryption Algorithm in Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc5753 RFC 5753>: Use of Elliptic Curve Cryptography (ECC) Algorithms in Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc5754 RFC 5754>: Using SHA2 Algorithms with Cryptographic Message Syntax
+-- * <https://tools.ietf.org/html/rfc3211 RFC 3211>: Password-based Encryption for CMS
+-- * <https://tools.ietf.org/html/rfc5083 RFC 5083>: Cryptographic Message Syntax (CMS) Authenticated-Enveloped-Data Content Type
+-- * <https://tools.ietf.org/html/rfc5084 RFC 5084>: Using AES-CCM and AES-GCM Authenticated Encryption in the Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc6476 RFC 6476>: Using Message Authentication Code (MAC) Encryption in the Cryptographic Message Syntax (CMS)
+-- * <https://tools.ietf.org/html/rfc8103 RFC 8103>: Using ChaCha20-Poly1305 Authenticated Encryption in the Cryptographic Message Syntax (CMS)
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS
+    ( ContentType(..)
+    , ContentInfo(..)
+    , getContentType
+    -- * Reading and writing PEM files
+    , module Crypto.Store.CMS.PEM
+    -- * Signed data
+    , SignatureValue
+    , SignatureAlg(..)
+    , SignedData(..)
+    , ProducerOfSI
+    , ConsumerOfSI
+    , signData
+    , verifySignedData
+    -- ** Signer information
+    , SignerInfo(..)
+    , SignerIdentifier(..)
+    , IssuerAndSerialNumber(..)
+    , certSigner
+    , withPublicKey
+    , withSignerKey
+    , withSignerCertificate
+    -- * Enveloped data
+    , EncryptedKey
+    , KeyEncryptionParams(..)
+    , KeyTransportParams(..)
+    , KeyAgreementParams(..)
+    , RecipientInfo(..)
+    , EnvelopedData(..)
+    , ProducerOfRI
+    , ConsumerOfRI
+    , envelopData
+    , openEnvelopedData
+    -- ** Key Transport recipients
+    , KTRecipientInfo(..)
+    , RecipientIdentifier(..)
+    , forKeyTransRecipient
+    , withRecipientKeyTrans
+    -- ** Key Agreement recipients
+    , KARecipientInfo(..)
+    , OriginatorIdentifierOrKey(..)
+    , OriginatorPublicKey
+    , RecipientEncryptedKey(..)
+    , KeyAgreeRecipientIdentifier(..)
+    , UserKeyingMaterial
+    , forKeyAgreeRecipient
+    , withRecipientKeyAgree
+    -- ** Key Encryption Key recipients
+    , KEKRecipientInfo(..)
+    , KeyIdentifier(..)
+    , OtherKeyAttribute(..)
+    , KeyEncryptionKey
+    , forKeyRecipient
+    , withRecipientKey
+     -- ** Password recipients
+   , PasswordRecipientInfo(..)
+    , forPasswordRecipient
+    , withRecipientPassword
+    -- * Digested data
+    , DigestProxy(..)
+    , DigestAlgorithm(..)
+    , DigestedData(..)
+    , digestData
+    , digestVerify
+    -- * Encrypted data
+    , ContentEncryptionKey
+    , ContentEncryptionCipher(..)
+    , ContentEncryptionAlg(..)
+    , ContentEncryptionParams
+    , EncryptedContent
+    , EncryptedData(..)
+    , generateEncryptionParams
+    , generateRC2EncryptionParams
+    , getContentEncryptionAlg
+    , encryptData
+    , decryptData
+    -- * Authenticated data
+    , AuthenticationKey
+    , MACAlgorithm(..)
+    , MessageAuthenticationCode
+    , AuthenticatedData(..)
+    , generateAuthenticatedData
+    , verifyAuthenticatedData
+    -- * Authenticated-enveloped data
+    , AuthContentEncryptionAlg(..)
+    , AuthContentEncryptionParams
+    , AuthEnvelopedData(..)
+    , generateAuthEnc128Params
+    , generateAuthEnc256Params
+    , generateChaChaPoly1305Params
+    , generateCCMParams
+    , generateGCMParams
+    , authEnvelopData
+    , openAuthEnvelopedData
+    -- * Key derivation
+    , Salt
+    , generateSalt
+    , KeyDerivationFunc(..)
+    , PBKDF2_PRF(..)
+    -- * Secret-key algorithms
+    , HasKeySize(..)
+    , generateKey
+    -- * RSA padding modes
+    , MaskGenerationFunc(..)
+    , OAEPParams(..)
+    , PSSParams(..)
+    -- * CMS attributes
+    , Attribute(..)
+    , findAttribute
+    , setAttribute
+    , filterAttributes
+    -- * Originator information
+    , OriginatorInfo(..)
+    , CertificateChoice(..)
+    , OtherCertificateFormat(..)
+    , RevocationInfoChoice(..)
+    , OtherRevocationInfoFormat(..)
+    -- * ASN.1 representation
+    , ASN1ObjectExact
+    ) where
+
+import Data.Maybe (isJust)
+import Data.List (nub, unzip3)
+
+import Crypto.Hash
+
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.AuthEnveloped
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.Enveloped
+import Crypto.Store.CMS.OriginatorInfo
+import Crypto.Store.CMS.Info
+import Crypto.Store.CMS.PEM
+import Crypto.Store.CMS.Signed
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+
+
+-- DigestedData
+
+-- | Add a digested-data layer on the specified content info.
+digestData :: DigestAlgorithm -> ContentInfo -> ContentInfo
+digestData (DigestAlgorithm alg) ci = DigestedDataCI dd
+  where dd = DigestedData
+                 { ddDigestAlgorithm = alg
+                 , ddContentInfo     = ci
+                 , ddDigest          = hash (encapsulate ci)
+                 }
+
+-- | Return the inner content info but only if the digest is valid.
+digestVerify :: DigestedData -> Maybe ContentInfo
+digestVerify DigestedData{..} =
+    if ddDigest == hash (encapsulate ddContentInfo)
+        then Just ddContentInfo
+        else Nothing
+
+
+-- EncryptedData
+
+-- | Add an encrypted-data layer on the specified content info.  The content is
+-- encrypted with specified key and algorithm.
+--
+-- Some optional attributes can be added but will not be encrypted.
+encryptData :: ContentEncryptionKey
+            -> ContentEncryptionParams
+            -> [Attribute]
+            -> ContentInfo
+            -> Either StoreError ContentInfo
+encryptData key params attrs ci =
+    EncryptedDataCI . build <$> contentEncrypt key params (encapsulate ci)
+  where
+    build ec = EncryptedData
+                   { edContentType = getContentType ci
+                   , edContentEncryptionParams = params
+                   , edEncryptedContent = ec
+                   , edUnprotectedAttrs = attrs
+                   }
+
+-- | Decrypt an encrypted content info using the specified key.
+decryptData :: ContentEncryptionKey
+            -> EncryptedData
+            -> Either StoreError ContentInfo
+decryptData key EncryptedData{..} = do
+    decrypted <- contentDecrypt key edContentEncryptionParams edEncryptedContent
+    decapsulate edContentType decrypted
+
+
+-- EnvelopedData
+
+-- | Add an enveloped-data layer on the specified content info.  The content is
+-- encrypted with specified key and algorithm.  The key is then processed by
+-- one or several 'ProducerOfRI' functions to create recipient info elements.
+--
+-- Some optional attributes can be added but will not be encrypted.
+envelopData :: Applicative f
+            => OriginatorInfo
+            -> ContentEncryptionKey
+            -> ContentEncryptionParams
+            -> [ProducerOfRI f]
+            -> [Attribute]
+            -> ContentInfo
+            -> f (Either StoreError ContentInfo)
+envelopData oinfo key params envFns attrs ci =
+    f <$> (sequence <$> traverse ($ key) envFns)
+  where
+    ebs = contentEncrypt key params (encapsulate ci)
+    f ris = EnvelopedDataCI <$> (build <$> ebs <*> ris)
+    build bs ris = EnvelopedData
+                       { evOriginatorInfo = oinfo
+                       , evRecipientInfos = ris
+                       , evContentType = getContentType ci
+                       , evContentEncryptionParams = params
+                       , evEncryptedContent = bs
+                       , evUnprotectedAttrs = attrs
+                       }
+
+-- | Recover an enveloped content info using the specified 'ConsumerOfRI'
+-- function.
+openEnvelopedData :: Monad m
+                  => ConsumerOfRI m
+                  -> EnvelopedData
+                  -> m (Either StoreError ContentInfo)
+openEnvelopedData devFn EnvelopedData{..} = do
+    r <- riAttempts (map (fmap (>>= decr) . devFn) evRecipientInfos)
+    return (r >>= decapsulate ct)
+  where
+    ct       = evContentType
+    params   = evContentEncryptionParams
+    decr k   = contentDecrypt k params evEncryptedContent
+
+
+-- AuthenticatedData
+
+-- | Key used for authentication.
+type AuthenticationKey = ContentEncryptionKey
+
+-- | Add an authenticated-data layer on the specified content info.  The content
+-- is MACed with the specified key and algorithms.  The key is then processed by
+-- one or several 'ProducerOfRI' functions to create recipient info elements.
+--
+-- Two lists of optional attributes can be provided.  The attributes will be
+-- part of message authentication when provided in the first list.
+generateAuthenticatedData :: Applicative f
+                          => OriginatorInfo
+                          -> AuthenticationKey
+                          -> MACAlgorithm
+                          -> Maybe DigestAlgorithm
+                          -> [ProducerOfRI f]
+                          -> [Attribute]
+                          -> [Attribute]
+                          -> ContentInfo
+                          -> f (Either StoreError ContentInfo)
+generateAuthenticatedData oinfo key macAlg digAlg envFns aAttrs uAttrs ci =
+    f <$> (sequence <$> traverse ($ key) envFns)
+  where
+    msg = encapsulate ci
+    ct  = getContentType ci
+
+    (aAttrs', input) =
+        case digAlg of
+            Nothing  -> (aAttrs, msg)
+            Just dig ->
+                let md = digest dig msg
+                    l  = setContentTypeAttr ct $ setMessageDigestAttr md aAttrs
+                in (l, encodeAuthAttrs l)
+
+    ebs   = mac macAlg key input
+    f ris = AuthenticatedDataCI <$> (build ebs <$> ris)
+    build authTag ris = AuthenticatedData
+                            { adOriginatorInfo = oinfo
+                            , adRecipientInfos = ris
+                            , adMACAlgorithm = macAlg
+                            , adDigestAlgorithm = digAlg
+                            , adContentInfo = ci
+                            , adAuthAttrs = aAttrs'
+                            , adMAC = authTag
+                            , adUnauthAttrs = uAttrs
+                            }
+
+-- | Verify the integrity of an authenticated content info using the specified
+-- 'ConsumerOfRI' function.  The inner content info is returned only if the MAC
+-- could be verified.
+verifyAuthenticatedData :: Monad m
+                        => ConsumerOfRI m
+                        -> AuthenticatedData
+                        -> m (Either StoreError ContentInfo)
+verifyAuthenticatedData devFn AuthenticatedData{..} =
+    riAttempts (map (fmap (>>= unwrap) . devFn) adRecipientInfos)
+  where
+    msg = encapsulate adContentInfo
+    ct  = getContentType adContentInfo
+
+    noAttr    = null adAuthAttrs
+    mdMatch   = case adDigestAlgorithm of
+                    Nothing  -> False
+                    Just dig -> mdAttr == Just (digest dig msg)
+    attrMatch = ctAttr == Just ct && mdMatch
+    mdAttr    = getMessageDigestAttr adAuthAttrs
+    ctAttr    = getContentTypeAttr adAuthAttrs
+    input     = if noAttr then msg else encodeAuthAttrs adAuthAttrs
+
+    unwrap k
+        | isJust adDigestAlgorithm && noAttr  = Left (InvalidInput "Missing auth attributes")
+        | not noAttr && not attrMatch         = Left (InvalidInput "Invalid auth attributes")
+        | adMAC /= mac adMACAlgorithm k input = Left BadContentMAC
+        | otherwise                           = Right adContentInfo
+
+
+-- AuthEnvelopedData
+
+-- | Add an authenticated-enveloped-data layer on the specified content info.
+-- The content is encrypted with specified key and algorithm.  The key is then
+-- processed by one or several 'ProducerOfRI' functions to create recipient info
+-- elements.
+--
+-- Some attributes can be added but will not be encrypted.  The attributes
+-- will be part of message authentication when provided in the first list.
+authEnvelopData :: Applicative f
+                => OriginatorInfo
+                -> ContentEncryptionKey
+                -> AuthContentEncryptionParams
+                -> [ProducerOfRI f]
+                -> [Attribute]
+                -> [Attribute]
+                -> ContentInfo
+                -> f (Either StoreError ContentInfo)
+authEnvelopData oinfo key params envFns aAttrs uAttrs ci =
+    f <$> (sequence <$> traverse ($ key) envFns)
+  where
+    raw = encodeASN1Object params
+    aad = encodeAuthAttrs aAttrs
+    ebs = authContentEncrypt key params raw aad (encapsulate ci)
+    f ris = AuthEnvelopedDataCI <$> (build <$> ebs <*> ris)
+    build (authTag, bs) ris = AuthEnvelopedData
+                       { aeOriginatorInfo = oinfo
+                       , aeRecipientInfos = ris
+                       , aeContentType = getContentType ci
+                       , aeContentEncryptionParams = ASN1ObjectExact params raw
+                       , aeEncryptedContent = bs
+                       , aeAuthAttrs = aAttrs
+                       , aeMAC = authTag
+                       , aeUnauthAttrs = uAttrs
+                       }
+
+-- | Recover an authenticated-enveloped content info using the specified
+-- 'ConsumerOfRI' function.
+openAuthEnvelopedData :: Monad m
+                      => ConsumerOfRI m
+                      -> AuthEnvelopedData
+                      -> m (Either StoreError ContentInfo)
+openAuthEnvelopedData devFn AuthEnvelopedData{..} = do
+    r <- riAttempts (map (fmap (>>= decr) . devFn) aeRecipientInfos)
+    return (r >>= decapsulate ct)
+  where
+    ct       = aeContentType
+    params   = exactObject aeContentEncryptionParams
+    raw      = exactObjectRaw aeContentEncryptionParams
+    aad      = encodeAuthAttrs aeAuthAttrs
+    decr k   = authContentDecrypt k params raw aad aeEncryptedContent aeMAC
+
+
+-- SignedData
+
+-- | Add a signed-data layer on the specified content info.  The content is
+-- processed by one or several 'ProducerOfSI' functions to create signer info
+-- elements.
+signData :: Applicative f
+         => [ProducerOfSI f] -> ContentInfo -> f (Either StoreError ContentInfo)
+signData sigFns ci =
+    f <$> (sequence <$> traverse (\fn -> fn ct msg) sigFns)
+  where
+    msg = encapsulate ci
+    ct  = getContentType ci
+    f   = fmap (SignedDataCI . build . unzip3)
+
+    build (sis, certLists, crlLists) =
+        SignedData
+            { sdDigestAlgorithms = nub (map siDigestAlgorithm sis)
+            , sdContentInfo = ci
+            , sdCertificates = concat certLists
+            , sdCRLs = concat crlLists
+            , sdSignerInfos = sis
+            }
+
+-- | Verify a signed content info using the specified 'ConsumerOfSI' function.
+-- Verification of at least one signer info must be successful in order to
+-- return the inner content info.
+verifySignedData :: Monad m
+                 => ConsumerOfSI m -> SignedData -> m (Maybe ContentInfo)
+verifySignedData verFn SignedData{..} =
+    f <$> siAttemps valid sdSignerInfos
+  where
+    msg      = encapsulate sdContentInfo
+    ct       = getContentType sdContentInfo
+    valid si = verFn ct msg si sdCertificates sdCRLs
+    f bool   = if bool then Just sdContentInfo else Nothing
+
+
+-- Utilities
+
+riAttempts :: Monad m => [m (Either StoreError b)] -> m (Either StoreError b)
+riAttempts []       = return (Left NoRecipientInfoFound)
+riAttempts [single] = single
+riAttempts list     = loop list
+  where
+    loop []     = return (Left NoRecipientInfoMatched)
+    loop (x:xs) = x >>= orTail xs
+
+    orTail xs (Left _)  = loop xs
+    orTail _  success   = return success
+
+siAttemps :: Monad m => (a -> m Bool) -> [a] -> m Bool
+siAttemps _ []     = pure False
+siAttemps f (x:xs) = f x >>= orTail
+  where orTail bool = if bool then return True else siAttemps f xs
diff --git a/src/Crypto/Store/CMS/Algorithms.hs b/src/Crypto/Store/CMS/Algorithms.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Algorithms.hs
@@ -0,0 +1,1923 @@
+-- |
+-- Module      : Crypto.Store.CMS.Algorithms
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Cryptographic Message Syntax algorithms
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TypeFamilies #-}
+module Crypto.Store.CMS.Algorithms
+    ( DigestAlgorithm(..)
+    , DigestProxy(..)
+    , digest
+    , MessageAuthenticationCode
+    , MACAlgorithm(..)
+    , mac
+    , HasKeySize(..)
+    , getMaximumKeySize
+    , validateKeySize
+    , generateKey
+    , ContentEncryptionCipher(..)
+    , ContentEncryptionAlg(..)
+    , ContentEncryptionParams(..)
+    , generateEncryptionParams
+    , generateRC2EncryptionParams
+    , getContentEncryptionAlg
+    , proxyBlockSize
+    , contentEncrypt
+    , contentDecrypt
+    , AuthContentEncryptionAlg(..)
+    , AuthContentEncryptionParams
+    , generateAuthEnc128Params
+    , generateAuthEnc256Params
+    , generateChaChaPoly1305Params
+    , generateCCMParams
+    , generateGCMParams
+    , authContentEncrypt
+    , authContentDecrypt
+    , PBKDF2_PRF(..)
+    , prf
+    , Salt
+    , generateSalt
+    , KeyDerivationFunc(..)
+    , kdfKeyLength
+    , kdfDerive
+    , KeyEncryptionParams(..)
+    , keyEncrypt
+    , keyDecrypt
+    , OAEPParams(..)
+    , KeyTransportParams(..)
+    , transportEncrypt
+    , transportDecrypt
+    , KeyAgreementParams(..)
+    , ecdhGenerate
+    , ecdhEncrypt
+    , ecdhDecrypt
+    , MaskGenerationFunc(..)
+    , mgf
+    , SignatureValue
+    , PSSParams(..)
+    , SignatureAlg(..)
+    , signatureResolveHash
+    , signatureCheckHash
+    , signatureGenerate
+    , signatureVerify
+    ) where
+
+import Control.Applicative
+import Control.Monad (guard, when)
+
+import           Data.ASN1.BinaryEncoding
+import           Data.ASN1.OID
+import           Data.ASN1.Encoding
+import           Data.ASN1.Types
+import           Data.Bits
+import           Data.ByteArray (ByteArray, ByteArrayAccess)
+import qualified Data.ByteArray as B
+import           Data.ByteString (ByteString)
+import           Data.Maybe (fromMaybe)
+import           Data.Word
+import qualified Data.X509 as X509
+import           Data.X509.EC
+
+import qualified Crypto.Cipher.AES as Cipher
+import qualified Crypto.Cipher.CAST5 as Cipher
+import qualified Crypto.Cipher.Camellia as Cipher
+import qualified Crypto.Cipher.ChaChaPoly1305 as ChaChaPoly1305
+import qualified Crypto.Cipher.DES as Cipher
+import qualified Crypto.Cipher.TripleDES as Cipher
+import           Crypto.Cipher.Types
+import           Crypto.Data.Padding
+import           Crypto.Error
+import qualified Crypto.Hash as Hash
+import qualified Crypto.KDF.PBKDF2 as PBKDF2
+import qualified Crypto.KDF.Scrypt as Scrypt
+import qualified Crypto.MAC.HMAC as HMAC
+import qualified Crypto.MAC.Poly1305 as Poly1305
+import           Crypto.Number.Serialize
+import qualified Crypto.PubKey.DSA as DSA
+import qualified Crypto.PubKey.ECC.DH as ECDH
+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
+import qualified Crypto.PubKey.ECC.Types as ECC
+import qualified Crypto.PubKey.MaskGenFunction as MGF
+import qualified Crypto.PubKey.RSA.PSS as RSAPSS
+import qualified Crypto.PubKey.RSA.OAEP as RSAOAEP
+import qualified Crypto.PubKey.RSA.PKCS15 as RSA
+import           Crypto.Random
+
+import Foreign.Ptr (Ptr)
+import Foreign.Storable
+
+import           Crypto.Store.ASN1.Generate
+import           Crypto.Store.ASN1.Parse
+import           Crypto.Store.CMS.Util
+import           Crypto.Store.Cipher.RC2
+import           Crypto.Store.Error
+import qualified Crypto.Store.KeyWrap.AES as AES_KW
+import qualified Crypto.Store.KeyWrap.TripleDES as TripleDES_KW
+import qualified Crypto.Store.KeyWrap.RC2 as RC2_KW
+import           Crypto.Store.Util
+
+
+-- Hash functions
+
+-- | CMS digest proxy.  Acts like 'Data.Proxy.Proxy', i.e. provides a hash
+-- algorithm as type parameter.  The GADT constructors map to known algorithms.
+data DigestProxy hashAlg where
+    -- | MD2
+    MD2    :: DigestProxy Hash.MD2
+    -- | MD4
+    MD4    :: DigestProxy Hash.MD4
+    -- | MD5
+    MD5    :: DigestProxy Hash.MD5
+    -- | SHA-1
+    SHA1   :: DigestProxy Hash.SHA1
+    -- | SHA-224
+    SHA224 :: DigestProxy Hash.SHA224
+    -- | SHA-256
+    SHA256 :: DigestProxy Hash.SHA256
+    -- | SHA-384
+    SHA384 :: DigestProxy Hash.SHA384
+    -- | SHA-512
+    SHA512 :: DigestProxy Hash.SHA512
+
+deriving instance Show (DigestProxy hashAlg)
+deriving instance Eq (DigestProxy hashAlg)
+
+-- | CMS digest algorithm.
+data DigestAlgorithm =
+    forall hashAlg . Hash.HashAlgorithm hashAlg
+        => DigestAlgorithm (DigestProxy hashAlg)
+
+instance Show DigestAlgorithm where
+    show (DigestAlgorithm a) = show a
+
+instance Eq DigestAlgorithm where
+    DigestAlgorithm MD2          == DigestAlgorithm MD2          = True
+    DigestAlgorithm MD4          == DigestAlgorithm MD4          = True
+    DigestAlgorithm MD5          == DigestAlgorithm MD5          = True
+    DigestAlgorithm SHA1         == DigestAlgorithm SHA1         = True
+    DigestAlgorithm SHA224       == DigestAlgorithm SHA224       = True
+    DigestAlgorithm SHA256       == DigestAlgorithm SHA256       = True
+    DigestAlgorithm SHA384       == DigestAlgorithm SHA384       = True
+    DigestAlgorithm SHA512       == DigestAlgorithm SHA512       = True
+    _                            == _                            = False
+
+instance Enumerable DigestAlgorithm where
+    values = [ DigestAlgorithm MD2
+             , DigestAlgorithm MD4
+             , DigestAlgorithm MD5
+             , DigestAlgorithm SHA1
+             , DigestAlgorithm SHA224
+             , DigestAlgorithm SHA256
+             , DigestAlgorithm SHA384
+             , DigestAlgorithm SHA512
+             ]
+
+instance OIDable DigestAlgorithm where
+    getObjectID (DigestAlgorithm MD2)    = [1,2,840,113549,2,2]
+    getObjectID (DigestAlgorithm MD4)    = [1,2,840,113549,2,4]
+    getObjectID (DigestAlgorithm MD5)    = [1,2,840,113549,2,5]
+    getObjectID (DigestAlgorithm SHA1)   = [1,3,14,3,2,26]
+    getObjectID (DigestAlgorithm SHA224) = [2,16,840,1,101,3,4,2,4]
+    getObjectID (DigestAlgorithm SHA256) = [2,16,840,1,101,3,4,2,1]
+    getObjectID (DigestAlgorithm SHA384) = [2,16,840,1,101,3,4,2,2]
+    getObjectID (DigestAlgorithm SHA512) = [2,16,840,1,101,3,4,2,3]
+
+instance OIDNameable DigestAlgorithm where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+instance AlgorithmId DigestAlgorithm where
+    type AlgorithmType DigestAlgorithm = DigestAlgorithm
+    algorithmName _  = "digest algorithm"
+    algorithmType    = id
+
+    -- MD5 has NULL parameter, other algorithms have no parameter
+    parameterASN1S (DigestAlgorithm MD5) = gNull
+    parameterASN1S _                     = id
+
+    parseParameter p = getNextMaybe nullOrNothing >> return p
+
+-- | Compute the digest of a message.
+digest :: ByteArrayAccess message => DigestAlgorithm -> message -> ByteString
+digest (DigestAlgorithm hashAlg) message = B.convert (doHash hashAlg message)
+
+doHash :: (Hash.HashAlgorithm hashAlg, ByteArrayAccess ba)
+       => proxy hashAlg -> ba -> Hash.Digest hashAlg
+doHash _ = Hash.hash
+
+hashFromProxy :: proxy a -> a
+hashFromProxy _ = undefined
+
+
+-- Cipher-like things
+
+-- | Algorithms that are based on a secret key.  This includes ciphers but also
+-- MAC algorithms.
+class HasKeySize params where
+    -- | Get a specification of the key sizes allowed by the algorithm.
+    getKeySizeSpecifier :: params -> KeySizeSpecifier
+
+-- | Return the maximum key size for the specified algorithm.
+getMaximumKeySize :: HasKeySize params => params -> Int
+getMaximumKeySize params =
+    case getKeySizeSpecifier params of
+        KeySizeRange _ n -> n
+        KeySizeEnum  l   -> maximum l
+        KeySizeFixed n   -> n
+
+-- | Return 'True' if the specified key size is valid for the specified
+-- algorithm.
+validateKeySize :: HasKeySize params => params -> Int -> Bool
+validateKeySize params len =
+    case getKeySizeSpecifier params of
+        KeySizeRange a b -> a <= len && len <= b
+        KeySizeEnum  l   -> len `elem` l
+        KeySizeFixed n   -> len == n
+
+-- | Generate a random key suitable for the specified algorithm.  This uses the
+-- maximum size allowed by the parameters.
+generateKey :: (HasKeySize params, MonadRandom m, ByteArray key)
+            => params -> m key
+generateKey params = getRandomBytes (getMaximumKeySize params)
+
+
+-- MAC
+
+-- | Message authentication code.  Equality is time constant.
+type MessageAuthenticationCode = AuthTag
+
+-- | Message Authentication Code (MAC) Algorithm.
+data MACAlgorithm
+    = forall hashAlg . Hash.HashAlgorithm hashAlg
+        => HMAC (DigestProxy hashAlg)
+
+deriving instance Show MACAlgorithm
+
+instance Eq MACAlgorithm where
+    HMAC a1 == HMAC a2 = DigestAlgorithm a1 == DigestAlgorithm a2
+
+instance Enumerable MACAlgorithm where
+    values = [ HMAC MD5
+             , HMAC SHA1
+             , HMAC SHA224
+             , HMAC SHA256
+             , HMAC SHA384
+             , HMAC SHA512
+             ]
+
+instance OIDable MACAlgorithm where
+    getObjectID (HMAC MD5)    = [1,3,6,1,5,5,8,1,1]
+    getObjectID (HMAC SHA1)   = [1,3,6,1,5,5,8,1,2]
+    getObjectID (HMAC SHA224) = [1,2,840,113549,2,8]
+    getObjectID (HMAC SHA256) = [1,2,840,113549,2,9]
+    getObjectID (HMAC SHA384) = [1,2,840,113549,2,10]
+    getObjectID (HMAC SHA512) = [1,2,840,113549,2,11]
+
+    getObjectID ty = error ("Unsupported MACAlgorithm: " ++ show ty)
+
+instance OIDNameable MACAlgorithm where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+instance AlgorithmId MACAlgorithm where
+    type AlgorithmType MACAlgorithm = MACAlgorithm
+    algorithmName _  = "mac algorithm"
+    algorithmType    = id
+    parameterASN1S _ = id
+    parseParameter p = getNextMaybe nullOrNothing >> return p
+
+instance HasKeySize MACAlgorithm where
+    getKeySizeSpecifier (HMAC a) = KeySizeFixed (digestSizeFromProxy a)
+      where digestSizeFromProxy = Hash.hashDigestSize . hashFromProxy
+
+-- | Invoke the MAC function.
+mac :: (ByteArrayAccess key, ByteArrayAccess message)
+     => MACAlgorithm -> key -> message -> MessageAuthenticationCode
+mac (HMAC alg) = hmacWith alg
+  where
+    hmacWith p key = AuthTag . B.convert . runHMAC p key
+
+    runHMAC :: (Hash.HashAlgorithm a, ByteArrayAccess k, ByteArrayAccess m)
+        => proxy a -> k -> m -> HMAC.HMAC a
+    runHMAC _ = HMAC.hmac
+
+
+-- Content encryption
+
+-- | CMS content encryption cipher.
+data ContentEncryptionCipher cipher where
+    -- | DES
+    DES         :: ContentEncryptionCipher Cipher.DES
+    -- | Triple-DES with 2 keys used in alternative direction
+    DES_EDE2    :: ContentEncryptionCipher Cipher.DES_EDE2
+    -- | Triple-DES with 3 keys used in alternative direction
+    DES_EDE3    :: ContentEncryptionCipher Cipher.DES_EDE3
+    -- | AES with 128-bit key
+    AES128      :: ContentEncryptionCipher Cipher.AES128
+    -- | AES with 192-bit key
+    AES192      :: ContentEncryptionCipher Cipher.AES192
+    -- | AES with 256-bit key
+    AES256      :: ContentEncryptionCipher Cipher.AES256
+    -- | CAST5 (aka CAST-128) with key between 40 and 128 bits
+    CAST5       :: ContentEncryptionCipher Cipher.CAST5
+    -- | Camellia with 128-bit key
+    Camellia128 :: ContentEncryptionCipher Cipher.Camellia128
+
+deriving instance Show (ContentEncryptionCipher cipher)
+deriving instance Eq (ContentEncryptionCipher cipher)
+
+cecI :: ContentEncryptionCipher c -> Int
+cecI DES         = 0
+cecI DES_EDE2    = 1
+cecI DES_EDE3    = 2
+cecI AES128      = 3
+cecI AES192      = 4
+cecI AES256      = 5
+cecI CAST5       = 6
+cecI Camellia128 = 7
+
+getCipherKeySizeSpecifier :: Cipher cipher => proxy cipher -> KeySizeSpecifier
+getCipherKeySizeSpecifier = cipherKeySize . cipherFromProxy
+
+-- | Cipher and mode of operation for content encryption.
+data ContentEncryptionAlg
+    = forall c . BlockCipher c => ECB (ContentEncryptionCipher c)
+      -- ^ Electronic Codebook
+    | forall c . BlockCipher c => CBC (ContentEncryptionCipher c)
+      -- ^ Cipher Block Chaining
+    | CBC_RC2
+      -- ^ RC2 in CBC mode
+    | forall c . BlockCipher c => CFB (ContentEncryptionCipher c)
+      -- ^ Cipher Feedback
+    | forall c . BlockCipher c => CTR (ContentEncryptionCipher c)
+      -- ^ Counter
+
+instance Show ContentEncryptionAlg where
+    show (ECB c) = shows c "_ECB"
+    show (CBC c) = shows c "_CBC"
+    show CBC_RC2 = "RC2_CBC"
+    show (CFB c) = shows c "_CFB"
+    show (CTR c) = shows c "_CTR"
+
+instance Enumerable ContentEncryptionAlg where
+    values = [ CBC DES
+             , CBC DES_EDE3
+             , CBC AES128
+             , CBC AES192
+             , CBC AES256
+             , CBC CAST5
+             , CBC Camellia128
+             , CBC_RC2
+
+             , ECB DES
+             , ECB AES128
+             , ECB AES192
+             , ECB AES256
+             , ECB Camellia128
+
+             , CFB DES
+             , CFB AES128
+             , CFB AES192
+             , CFB AES256
+             , CFB Camellia128
+
+             , CTR Camellia128
+             ]
+
+instance OIDable ContentEncryptionAlg where
+    getObjectID (CBC DES)          = [1,3,14,3,2,7]
+    getObjectID (CBC DES_EDE3)     = [1,2,840,113549,3,7]
+    getObjectID (CBC AES128)       = [2,16,840,1,101,3,4,1,2]
+    getObjectID (CBC AES192)       = [2,16,840,1,101,3,4,1,22]
+    getObjectID (CBC AES256)       = [2,16,840,1,101,3,4,1,42]
+    getObjectID (CBC CAST5)        = [1,2,840,113533,7,66,10]
+    getObjectID (CBC Camellia128)  = [1,2,392,200011,61,1,1,1,2]
+    getObjectID CBC_RC2            = [1,2,840,113549,3,2]
+
+    getObjectID (ECB DES)          = [1,3,14,3,2,6]
+    getObjectID (ECB AES128)       = [2,16,840,1,101,3,4,1,1]
+    getObjectID (ECB AES192)       = [2,16,840,1,101,3,4,1,21]
+    getObjectID (ECB AES256)       = [2,16,840,1,101,3,4,1,41]
+    getObjectID (ECB Camellia128)  = [0,3,4401,5,3,1,9,1]
+
+    getObjectID (CFB DES)          = [1,3,14,3,2,9]
+    getObjectID (CFB AES128)       = [2,16,840,1,101,3,4,1,4]
+    getObjectID (CFB AES192)       = [2,16,840,1,101,3,4,1,24]
+    getObjectID (CFB AES256)       = [2,16,840,1,101,3,4,1,44]
+    getObjectID (CFB Camellia128)  = [0,3,4401,5,3,1,9,4]
+
+    getObjectID (CTR Camellia128)  = [0,3,4401,5,3,1,9,9]
+
+    getObjectID ty = error ("Unsupported ContentEncryptionAlg: " ++ show ty)
+
+instance OIDNameable ContentEncryptionAlg where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Content encryption algorithm with associated parameters (i.e. the
+-- initialization vector).
+--
+-- A value can be generated with 'generateEncryptionParams'.
+data ContentEncryptionParams
+    = forall c . BlockCipher c => ParamsECB (ContentEncryptionCipher c)
+      -- ^ Electronic Codebook
+    | forall c . BlockCipher c => ParamsCBC (ContentEncryptionCipher c) (IV c)
+      -- ^ Cipher Block Chaining
+    | ParamsCBCRC2 Int (IV RC2)
+      -- ^ RC2 in CBC mode
+    | forall c . BlockCipher c => ParamsCFB (ContentEncryptionCipher c) (IV c)
+      -- ^ Cipher Feedback
+    | forall c . BlockCipher c => ParamsCTR (ContentEncryptionCipher c) (IV c)
+      -- ^ Counter
+
+instance Show ContentEncryptionParams where
+    show = show . getContentEncryptionAlg
+
+instance Eq ContentEncryptionParams where
+    ParamsECB c1        == ParamsECB c2        = cecI c1 == cecI c2
+    ParamsCBC c1 iv1    == ParamsCBC c2 iv2    = cecI c1 == cecI c2 && iv1 `B.eq` iv2
+    ParamsCBCRC2 i1 iv1 == ParamsCBCRC2 i2 iv2 = i1 == i2 && iv1 `B.eq` iv2
+    ParamsCFB c1 iv1    == ParamsCFB c2 iv2    = cecI c1 == cecI c2 && iv1 `B.eq` iv2
+    ParamsCTR c1 iv1    == ParamsCTR c2 iv2    = cecI c1 == cecI c2 && iv1 `B.eq` iv2
+    _                   == _                   = False
+
+instance HasKeySize ContentEncryptionParams where
+    getKeySizeSpecifier (ParamsECB c)      = getCipherKeySizeSpecifier c
+    getKeySizeSpecifier (ParamsCBC c _)    = getCipherKeySizeSpecifier c
+    getKeySizeSpecifier (ParamsCBCRC2 i _) = KeySizeFixed $ (i + 7) `div` 8
+    getKeySizeSpecifier (ParamsCFB c _)    = getCipherKeySizeSpecifier c
+    getKeySizeSpecifier (ParamsCTR c _)    = getCipherKeySizeSpecifier c
+
+instance ASN1Elem e => ProduceASN1Object e ContentEncryptionParams where
+    asn1s param =
+        asn1Container Sequence (oid . params)
+      where
+        oid    = gOID (getObjectID $ getContentEncryptionAlg param)
+        params = ceParameterASN1S param
+
+instance Monoid e => ParseASN1Object e ContentEncryptionParams where
+    parse = onNextContainer Sequence $ do
+        OID oid <- getNext
+        withObjectID "content encryption algorithm" oid parseCEParameter
+
+ceParameterASN1S :: ASN1Elem e => ContentEncryptionParams -> ASN1Stream e
+ceParameterASN1S (ParamsECB _)         = id
+ceParameterASN1S (ParamsCBC _ iv)      = gOctetString (B.convert iv)
+ceParameterASN1S (ParamsCBCRC2 len iv) = rc2ParameterASN1S len iv
+ceParameterASN1S (ParamsCFB _ iv)      = gOctetString (B.convert iv)
+ceParameterASN1S (ParamsCTR _ iv)      = gOctetString (B.convert iv)
+
+parseCEParameter :: Monoid e
+                 => ContentEncryptionAlg -> ParseASN1 e ContentEncryptionParams
+parseCEParameter (ECB c) = getMany getNext >> return (ParamsECB c)
+parseCEParameter (CBC c) = ParamsCBC c <$> (getNext >>= getIV)
+parseCEParameter CBC_RC2 = parseRC2Parameter
+parseCEParameter (CFB c) = ParamsCFB c <$> (getNext >>= getIV)
+parseCEParameter (CTR c) = ParamsCTR c <$> (getNext >>= getIV)
+
+getIV :: BlockCipher cipher => ASN1 -> ParseASN1 e (IV cipher)
+getIV (OctetString ivBs) =
+    case makeIV ivBs of
+        Nothing -> throwParseError "Bad IV in parsed parameters"
+        Just v  -> return v
+getIV _ = throwParseError "No IV in parsed parameter or incorrect format"
+
+rc2ParameterASN1S :: ASN1Elem e => Int -> IV RC2 -> ASN1Stream e
+rc2ParameterASN1S len iv
+    | len == 32 = gIV
+    | otherwise = asn1Container Sequence (rc2VersionASN1 len . gIV)
+  where gIV = gOctetString (B.convert iv)
+
+parseRC2Parameter :: Monoid e => ParseASN1 e ContentEncryptionParams
+parseRC2Parameter = parseOnlyIV 32 <|> parseFullParams
+  where
+    parseOnlyIV len = ParamsCBCRC2 len <$> (getNext >>= getIV)
+    parseFullParams = onNextContainer Sequence $
+        parseRC2Version >>= parseOnlyIV
+
+-- | Get the content encryption algorithm.
+getContentEncryptionAlg :: ContentEncryptionParams -> ContentEncryptionAlg
+getContentEncryptionAlg (ParamsECB c)      = ECB c
+getContentEncryptionAlg (ParamsCBC c _)    = CBC c
+getContentEncryptionAlg (ParamsCBCRC2 _ _) = CBC_RC2
+getContentEncryptionAlg (ParamsCFB c _)    = CFB c
+getContentEncryptionAlg (ParamsCTR c _)    = CTR c
+
+-- | Generate random parameters for the specified content encryption algorithm.
+generateEncryptionParams :: MonadRandom m
+                         => ContentEncryptionAlg -> m ContentEncryptionParams
+generateEncryptionParams (ECB c) = return (ParamsECB c)
+generateEncryptionParams (CBC c) = ParamsCBC c <$> ivGenerate undefined
+generateEncryptionParams CBC_RC2 = ParamsCBCRC2 128 <$> ivGenerate undefined
+generateEncryptionParams (CFB c) = ParamsCFB c <$> ivGenerate undefined
+generateEncryptionParams (CTR c) = ParamsCTR c <$> ivGenerate undefined
+
+-- | Generate random RC2 parameters with the specified effective key length (in
+-- bits).
+generateRC2EncryptionParams :: MonadRandom m
+                            => Int -> m ContentEncryptionParams
+generateRC2EncryptionParams len = ParamsCBCRC2 len <$> ivGenerate undefined
+
+-- | Encrypt a bytearray with the specified content encryption key and
+-- algorithm.
+contentEncrypt :: (ByteArray cek, ByteArray ba)
+               => cek
+               -> ContentEncryptionParams
+               -> ba -> Either StoreError ba
+contentEncrypt key params bs =
+    case params of
+        ParamsECB cipher    -> getCipher cipher key >>= (\c -> force $ ecbEncrypt c    $ padded c bs)
+        ParamsCBC cipher iv -> getCipher cipher key >>= (\c -> force $ cbcEncrypt c iv $ padded c bs)
+        ParamsCBCRC2 len iv -> getRC2Cipher len key >>= (\c -> force $ cbcEncrypt c iv $ padded c bs)
+        ParamsCFB cipher iv -> getCipher cipher key >>= (\c -> force $ cfbEncrypt c iv $ padded c bs)
+        ParamsCTR cipher iv -> getCipher cipher key >>= (\c -> force $ ctrCombine c iv $ padded c bs)
+  where
+    force x  = x `seq` Right x
+    padded c = pad (PKCS7 $ blockSize c)
+
+-- | Decrypt a bytearray with the specified content encryption key and
+-- algorithm.
+contentDecrypt :: (ByteArray cek, ByteArray ba)
+               => cek
+               -> ContentEncryptionParams
+               -> ba -> Either StoreError ba
+contentDecrypt key params bs =
+    case params of
+        ParamsECB cipher    -> getCipher cipher key >>= (\c -> unpadded c (ecbDecrypt c    bs))
+        ParamsCBC cipher iv -> getCipher cipher key >>= (\c -> unpadded c (cbcDecrypt c iv bs))
+        ParamsCBCRC2 len iv -> getRC2Cipher len key >>= (\c -> unpadded c (cbcDecrypt c iv bs))
+        ParamsCFB cipher iv -> getCipher cipher key >>= (\c -> unpadded c (cfbDecrypt c iv bs))
+        ParamsCTR cipher iv -> getCipher cipher key >>= (\c -> unpadded c (ctrCombine c iv bs))
+  where
+    unpadded c decrypted =
+        case unpad (PKCS7 $ blockSize c) decrypted of
+            Nothing  -> Left DecryptionFailed
+            Just out -> Right out
+
+-- from RFC 2268 section 6
+rc2Table :: [Word8]
+rc2Table =
+    [ 0xbd, 0x56, 0xea, 0xf2, 0xa2, 0xf1, 0xac, 0x2a, 0xb0, 0x93, 0xd1, 0x9c, 0x1b, 0x33, 0xfd, 0xd0
+    , 0x30, 0x04, 0xb6, 0xdc, 0x7d, 0xdf, 0x32, 0x4b, 0xf7, 0xcb, 0x45, 0x9b, 0x31, 0xbb, 0x21, 0x5a
+    , 0x41, 0x9f, 0xe1, 0xd9, 0x4a, 0x4d, 0x9e, 0xda, 0xa0, 0x68, 0x2c, 0xc3, 0x27, 0x5f, 0x80, 0x36
+    , 0x3e, 0xee, 0xfb, 0x95, 0x1a, 0xfe, 0xce, 0xa8, 0x34, 0xa9, 0x13, 0xf0, 0xa6, 0x3f, 0xd8, 0x0c
+    , 0x78, 0x24, 0xaf, 0x23, 0x52, 0xc1, 0x67, 0x17, 0xf5, 0x66, 0x90, 0xe7, 0xe8, 0x07, 0xb8, 0x60
+    , 0x48, 0xe6, 0x1e, 0x53, 0xf3, 0x92, 0xa4, 0x72, 0x8c, 0x08, 0x15, 0x6e, 0x86, 0x00, 0x84, 0xfa
+    , 0xf4, 0x7f, 0x8a, 0x42, 0x19, 0xf6, 0xdb, 0xcd, 0x14, 0x8d, 0x50, 0x12, 0xba, 0x3c, 0x06, 0x4e
+    , 0xec, 0xb3, 0x35, 0x11, 0xa1, 0x88, 0x8e, 0x2b, 0x94, 0x99, 0xb7, 0x71, 0x74, 0xd3, 0xe4, 0xbf
+    , 0x3a, 0xde, 0x96, 0x0e, 0xbc, 0x0a, 0xed, 0x77, 0xfc, 0x37, 0x6b, 0x03, 0x79, 0x89, 0x62, 0xc6
+    , 0xd7, 0xc0, 0xd2, 0x7c, 0x6a, 0x8b, 0x22, 0xa3, 0x5b, 0x05, 0x5d, 0x02, 0x75, 0xd5, 0x61, 0xe3
+    , 0x18, 0x8f, 0x55, 0x51, 0xad, 0x1f, 0x0b, 0x5e, 0x85, 0xe5, 0xc2, 0x57, 0x63, 0xca, 0x3d, 0x6c
+    , 0xb4, 0xc5, 0xcc, 0x70, 0xb2, 0x91, 0x59, 0x0d, 0x47, 0x20, 0xc8, 0x4f, 0x58, 0xe0, 0x01, 0xe2
+    , 0x16, 0x38, 0xc4, 0x6f, 0x3b, 0x0f, 0x65, 0x46, 0xbe, 0x7e, 0x2d, 0x7b, 0x82, 0xf9, 0x40, 0xb5
+    , 0x1d, 0x73, 0xf8, 0xeb, 0x26, 0xc7, 0x87, 0x97, 0x25, 0x54, 0xb1, 0x28, 0xaa, 0x98, 0x9d, 0xa5
+    , 0x64, 0x6d, 0x7a, 0xd4, 0x10, 0x81, 0x44, 0xef, 0x49, 0xd6, 0xae, 0x2e, 0xdd, 0x76, 0x5c, 0x2f
+    , 0xa7, 0x1c, 0xc9, 0x09, 0x69, 0x9a, 0x83, 0xcf, 0x29, 0x39, 0xb9, 0xe9, 0x4c, 0xff, 0x43, 0xab
+    ]
+
+rc2Forward :: B.Bytes
+rc2Forward = B.pack rc2Table
+
+rc2Reverse :: B.Bytes
+rc2Reverse = B.allocAndFreeze (length rc2Table) (loop $ zip [0..] rc2Table)
+  where
+    loop :: [(Word8, Word8)] -> Ptr Word8 -> IO ()
+    loop []         _ = return ()
+    loop ((a,b):ts) p = pokeElemOff p (fromIntegral b) a >> loop ts p
+
+rc2VersionASN1 :: ASN1Elem e => Int -> ASN1Stream e
+rc2VersionASN1 len = gIntVal v
+  where
+    v | len < 0    = error "invalid RC2 effective key length"
+      | len >= 256 = fromIntegral len
+      | otherwise  = fromIntegral (B.index rc2Forward len)
+
+parseRC2Version :: Monoid e => ParseASN1 e Int
+parseRC2Version = do
+    IntVal i <- getNext
+    when (i < 0) $ throwParseError "Parsed invalid RC2 effective key length"
+    let j = fromIntegral i
+    return $ if i >= 256 then j else fromIntegral (B.index rc2Reverse j)
+
+
+-- Authenticated-content encryption
+
+-- | Cipher and mode of operation for authenticated-content encryption.
+data AuthContentEncryptionAlg
+    = AUTH_ENC_128
+      -- ^ authEnc with 128-bit key
+    | AUTH_ENC_256
+      -- ^ authEnc with 256-bit key
+    | CHACHA20_POLY1305
+      -- ^ ChaCha20-Poly1305 Authenticated Encryption
+    | forall c . BlockCipher c => CCM (ContentEncryptionCipher c)
+      -- ^ Counter with CBC-MAC
+    | forall c . BlockCipher c => GCM (ContentEncryptionCipher c)
+      -- ^ Galois Counter Mode
+
+instance Show AuthContentEncryptionAlg where
+    show AUTH_ENC_128 = "AUTH_ENC_128"
+    show AUTH_ENC_256 = "AUTH_ENC_256"
+    show CHACHA20_POLY1305 = "CHACHA20_POLY1305"
+    show (CCM c)      = shows c "_CCM"
+    show (GCM c)      = shows c "_GCM"
+
+instance Enumerable AuthContentEncryptionAlg where
+    values = [ AUTH_ENC_128
+             , AUTH_ENC_256
+             , CHACHA20_POLY1305
+
+             , CCM AES128
+             , CCM AES192
+             , CCM AES256
+
+             , GCM AES128
+             , GCM AES192
+             , GCM AES256
+             ]
+
+instance OIDable AuthContentEncryptionAlg where
+    getObjectID AUTH_ENC_128       = [1,2,840,113549,1,9,16,3,15]
+    getObjectID AUTH_ENC_256       = [1,2,840,113549,1,9,16,3,16]
+    getObjectID CHACHA20_POLY1305  = [1,2,840,113549,1,9,16,3,18]
+
+    getObjectID (CCM AES128)       = [2,16,840,1,101,3,4,1,7]
+    getObjectID (CCM AES192)       = [2,16,840,1,101,3,4,1,27]
+    getObjectID (CCM AES256)       = [2,16,840,1,101,3,4,1,47]
+
+    getObjectID (GCM AES128)       = [2,16,840,1,101,3,4,1,6]
+    getObjectID (GCM AES192)       = [2,16,840,1,101,3,4,1,26]
+    getObjectID (GCM AES256)       = [2,16,840,1,101,3,4,1,46]
+
+    getObjectID ty = error ("Unsupported AuthContentEncryptionAlg: " ++ show ty)
+
+instance OIDNameable AuthContentEncryptionAlg where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+data AuthEncParams = AuthEncParams
+    { prfAlgorithm :: PBKDF2_PRF
+    , encAlgorithm :: ContentEncryptionParams
+    , macAlgorithm :: MACAlgorithm
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e AuthEncParams where
+    asn1s AuthEncParams{..} = asn1Container Sequence (kdf . encAlg . macAlg)
+      where
+        kdf    = algorithmASN1S (Container Context 0) (asKDF prfAlgorithm)
+        encAlg = asn1s encAlgorithm
+        macAlg = algorithmASN1S Sequence macAlgorithm
+
+        asKDF algPrf = PBKDF2 { pbkdf2Salt = B.empty
+                              , pbkdf2IterationCount = 1
+                              , pbkdf2KeyLength = Nothing
+                              , pbkdf2Prf = algPrf
+                              }
+
+instance Monoid e => ParseASN1Object e AuthEncParams where
+    parse = onNextContainer Sequence $ do
+        kdf    <- parseAlgorithmMaybe (Container Context 0)
+        encAlg <- parse
+        macAlg <- parseAlgorithm Sequence
+        prfAlg <-
+            case kdf of
+                Nothing               -> return PBKDF2_SHA1
+                Just (PBKDF2 _ _ _ a) -> return a
+                Just other            -> throwParseError
+                    ("Unable to use " ++ show other ++ " in AuthEncParams")
+        return AuthEncParams { prfAlgorithm = prfAlg
+                             , encAlgorithm = encAlg
+                             , macAlgorithm = macAlg
+                             }
+
+-- | Authenticated-content encryption algorithm with associated parameters
+-- (i.e. the nonce).
+--
+-- A value can be generated with functions 'generateAuthEnc128Params',
+-- 'generateAuthEnc256Params', 'generateChaChaPoly1305Params',
+-- 'generateCCMParams' and 'generateGCMParams'.
+data AuthContentEncryptionParams
+    = Params_AUTH_ENC_128 AuthEncParams
+      -- ^ authEnc with 128-bit keying material
+    | Params_AUTH_ENC_256 AuthEncParams
+      -- ^ authEnc with 256-bit keying material
+    | Params_CHACHA20_POLY1305 ChaChaPoly1305.Nonce
+      -- ^ ChaCha20-Poly1305 Authenticated Encryption
+    | forall c . BlockCipher c => ParamsCCM (ContentEncryptionCipher c) B.Bytes CCM_M CCM_L
+      -- ^ Counter with CBC-MAC
+    | forall c . BlockCipher c => ParamsGCM (ContentEncryptionCipher c) B.Bytes Int
+      -- ^ Galois Counter Mode
+
+instance Show AuthContentEncryptionParams where
+    show = show . getAuthContentEncryptionAlg
+
+instance Eq AuthContentEncryptionParams where
+    Params_AUTH_ENC_128 p1 == Params_AUTH_ENC_128 p2 = p1 == p2
+    Params_AUTH_ENC_256 p1 == Params_AUTH_ENC_256 p2 = p1 == p2
+    Params_CHACHA20_POLY1305 iv1 == Params_CHACHA20_POLY1305 iv2 =
+        iv1 `B.eq` iv2
+
+    ParamsCCM c1 iv1 m1 l1 == ParamsCCM c2 iv2 m2 l2 =
+        cecI c1 == cecI c2 && iv1 == iv2 && (m1, l1) == (m2, l2)
+    ParamsGCM c1 iv1 len1  == ParamsGCM c2 iv2 len2  =
+        cecI c1 == cecI c2 && iv1 == iv2 && len1 == len2
+    _               == _               = False
+
+instance HasKeySize AuthContentEncryptionParams where
+    getKeySizeSpecifier (Params_AUTH_ENC_128 _) = KeySizeFixed 16
+    getKeySizeSpecifier (Params_AUTH_ENC_256 _) = KeySizeFixed 32
+    getKeySizeSpecifier (Params_CHACHA20_POLY1305 _) = KeySizeFixed 32
+    getKeySizeSpecifier (ParamsCCM c _ _ _)     = getCipherKeySizeSpecifier c
+    getKeySizeSpecifier (ParamsGCM c _ _)       = getCipherKeySizeSpecifier c
+
+instance ASN1Elem e => ProduceASN1Object e AuthContentEncryptionParams where
+    asn1s param =
+        asn1Container Sequence (oid . params)
+      where
+        oid    = gOID (getObjectID $ getAuthContentEncryptionAlg param)
+        params = aceParameterASN1S param
+
+instance Monoid e => ParseASN1Object e AuthContentEncryptionParams where
+    parse = onNextContainer Sequence $ do
+        OID oid <- getNext
+        withObjectID "authenticated-content encryption algorithm" oid
+            parseACEParameter
+
+aceParameterASN1S :: ASN1Elem e => AuthContentEncryptionParams -> ASN1Stream e
+aceParameterASN1S (Params_AUTH_ENC_128 p) = asn1s p
+aceParameterASN1S (Params_AUTH_ENC_256 p) = asn1s p
+aceParameterASN1S (Params_CHACHA20_POLY1305 iv) = gOctetString (B.convert iv)
+aceParameterASN1S (ParamsCCM _ iv m _) =
+    asn1Container Sequence (nonce . icvlen)
+  where
+    nonce  = gOctetString (B.convert iv)
+    icvlen = gIntVal (fromIntegral $ getM m)
+aceParameterASN1S (ParamsGCM _ iv len) =
+    asn1Container Sequence (nonce . icvlen)
+  where
+    nonce  = gOctetString (B.convert iv)
+    icvlen = gIntVal (fromIntegral len)
+
+parseACEParameter :: Monoid e
+                  => AuthContentEncryptionAlg
+                  -> ParseASN1 e AuthContentEncryptionParams
+parseACEParameter AUTH_ENC_128 = Params_AUTH_ENC_128 <$> parse
+parseACEParameter AUTH_ENC_256 = Params_AUTH_ENC_256 <$> parse
+parseACEParameter CHACHA20_POLY1305 = do
+    OctetString bs <- getNext
+    case ChaChaPoly1305.nonce12 bs of
+        CryptoPassed iv -> return (Params_CHACHA20_POLY1305 iv)
+        CryptoFailed e  ->
+            throwParseError $ "Parsed invalid ChaChaPoly1305 nonce: " ++ show e
+parseACEParameter (CCM c)      = onNextContainer Sequence $ do
+    OctetString iv <- getNext
+    let ivlen = B.length iv
+    when (ivlen < 7 || ivlen > 13) $
+        throwParseError $ "Parsed invalid CCM nonce length: " ++ show ivlen
+    let Just l = fromL (15 - ivlen)
+    m <- parseM
+    return (ParamsCCM c (B.convert iv) m l)
+parseACEParameter (GCM c)      = onNextContainer Sequence $ do
+    OctetString iv <- getNext
+    when (B.null iv) $
+        throwParseError "Parsed empty GCM nonce"
+    icvlen <- fromMaybe 12 <$> getNextMaybe intOrNothing
+    when (icvlen < 12 || icvlen > 16) $
+        throwParseError $ "Parsed invalid GCM ICV length: " ++ show icvlen
+    return (ParamsGCM c (B.convert iv) $ fromIntegral icvlen)
+
+-- | Get the authenticated-content encryption algorithm.
+getAuthContentEncryptionAlg :: AuthContentEncryptionParams
+                            -> AuthContentEncryptionAlg
+getAuthContentEncryptionAlg (Params_AUTH_ENC_128 _) = AUTH_ENC_128
+getAuthContentEncryptionAlg (Params_AUTH_ENC_256 _) = AUTH_ENC_256
+getAuthContentEncryptionAlg (Params_CHACHA20_POLY1305 _) = CHACHA20_POLY1305
+getAuthContentEncryptionAlg (ParamsCCM c _ _ _)     = CCM c
+getAuthContentEncryptionAlg (ParamsGCM c _ _)       = GCM c
+
+-- | Generate random 'AUTH_ENC_128' parameters with the specified algorithms.
+generateAuthEnc128Params :: MonadRandom m
+                         => PBKDF2_PRF -> ContentEncryptionAlg -> MACAlgorithm
+                         -> m AuthContentEncryptionParams
+generateAuthEnc128Params prfAlg cea macAlg = do
+    params <- generateEncryptionParams cea
+    return $ Params_AUTH_ENC_128 $
+        AuthEncParams { prfAlgorithm = prfAlg
+                      , encAlgorithm = params
+                      , macAlgorithm = macAlg
+                      }
+
+-- | Generate random 'AUTH_ENC_256' parameters with the specified algorithms.
+generateAuthEnc256Params :: MonadRandom m
+                         => PBKDF2_PRF -> ContentEncryptionAlg -> MACAlgorithm
+                         -> m AuthContentEncryptionParams
+generateAuthEnc256Params prfAlg cea macAlg = do
+    params <- generateEncryptionParams cea
+    return $ Params_AUTH_ENC_256 $
+        AuthEncParams { prfAlgorithm = prfAlg
+                      , encAlgorithm = params
+                      , macAlgorithm = macAlg
+                      }
+
+-- | Generate random 'CHACHA20_POLY1305' parameters.
+generateChaChaPoly1305Params :: MonadRandom m => m AuthContentEncryptionParams
+generateChaChaPoly1305Params = do
+    bs <- nonceGenerate 12
+    let iv = throwCryptoError (ChaChaPoly1305.nonce12 bs)
+    return (Params_CHACHA20_POLY1305 iv)
+
+-- | Generate random 'CCM' parameters for the specified cipher.
+generateCCMParams :: (MonadRandom m, BlockCipher c)
+                  => ContentEncryptionCipher c -> CCM_M -> CCM_L
+                  -> m AuthContentEncryptionParams
+generateCCMParams c m l = do
+    iv <- nonceGenerate (15 - getL l)
+    return (ParamsCCM c iv m l)
+
+-- | Generate random 'GCM' parameters for the specified cipher.
+generateGCMParams :: (MonadRandom m, BlockCipher c)
+                  => ContentEncryptionCipher c -> Int
+                  -> m AuthContentEncryptionParams
+generateGCMParams c l = do
+    iv <- nonceGenerate 12
+    return (ParamsGCM c iv l)
+
+-- | Encrypt a bytearray with the specified authenticated-content encryption
+-- key and algorithm.
+authContentEncrypt :: forall cek aad ba . (ByteArray cek, ByteArrayAccess aad, ByteArray ba)
+                   => cek
+                   -> AuthContentEncryptionParams -> ba
+                   -> aad -> ba -> Either StoreError (AuthTag, ba)
+authContentEncrypt key params paramsRaw aad bs =
+    case params of
+        Params_AUTH_ENC_128 p   -> checkAuthKey 16 key >> authEncrypt p
+        Params_AUTH_ENC_256 p   -> checkAuthKey 32 key >> authEncrypt p
+        Params_CHACHA20_POLY1305 iv -> ccpInit key iv aad >>= ccpEncrypt
+        ParamsCCM cipher iv m l -> getAEAD cipher key (AEAD_CCM msglen m l) iv >>= encrypt (getM m)
+        ParamsGCM cipher iv len -> getAEAD cipher key AEAD_GCM iv >>= encrypt len
+  where
+    msglen  = B.length bs
+    force x = x `seq` Right x
+
+    encrypt :: Int -> AEAD a -> Either StoreError (AuthTag, ba)
+    encrypt len aead = force $ aeadSimpleEncrypt aead aad bs len
+
+    ccpEncrypt :: ChaChaPoly1305.State -> Either a (AuthTag, ba)
+    ccpEncrypt state = force (found, encrypted)
+      where
+        (encrypted, state') = ChaChaPoly1305.encrypt bs state
+        found = ccpTag (ChaChaPoly1305.finalize state')
+
+    authEncrypt :: AuthEncParams -> Either StoreError (AuthTag, ba)
+    authEncrypt p@AuthEncParams{..} = do
+        let (encKey, macKey) = authKeys key p
+        encrypted <- contentEncrypt encKey encAlgorithm bs
+        let macMsg = paramsRaw `B.append` encrypted `B.append` B.convert aad
+            found  = mac macAlgorithm macKey macMsg
+        return (found, encrypted)
+
+-- | Decrypt a bytearray with the specified authenticated-content encryption key
+-- and algorithm.
+authContentDecrypt :: forall cek aad ba . (ByteArray cek, ByteArrayAccess aad, ByteArray ba)
+                   => cek
+                   -> AuthContentEncryptionParams -> ba
+                   -> aad -> ba -> AuthTag -> Either StoreError ba
+authContentDecrypt key params paramsRaw aad bs expected =
+    case params of
+        Params_AUTH_ENC_128 p   -> checkAuthKey 16 key >> authDecrypt p
+        Params_AUTH_ENC_256 p   -> checkAuthKey 32 key >> authDecrypt p
+        Params_CHACHA20_POLY1305 iv -> ccpInit key iv aad >>= ccpDecrypt
+        ParamsCCM cipher iv m l -> getAEAD cipher key (AEAD_CCM msglen m l) iv >>= decrypt
+        ParamsGCM cipher iv _   -> getAEAD cipher key AEAD_GCM iv >>= decrypt
+  where
+    msglen  = B.length bs
+    badMac  = Left BadContentMAC
+
+    decrypt :: AEAD a -> Either StoreError ba
+    decrypt aead = maybe badMac Right (aeadSimpleDecrypt aead aad bs expected)
+
+    ccpDecrypt :: ChaChaPoly1305.State -> Either StoreError ba
+    ccpDecrypt state
+        | found == expected = Right decrypted
+        | otherwise         = badMac
+      where
+        (decrypted, state') = ChaChaPoly1305.decrypt bs state
+        found = ccpTag (ChaChaPoly1305.finalize state')
+
+    authDecrypt :: AuthEncParams -> Either StoreError ba
+    authDecrypt p@AuthEncParams{..}
+        | found == expected = contentDecrypt encKey encAlgorithm bs
+        | otherwise         = badMac
+      where
+        (encKey, macKey) = authKeys key p
+        macMsg = paramsRaw `B.append` bs `B.append` B.convert aad
+        found  = mac macAlgorithm macKey macMsg
+
+getAEAD :: (BlockCipher cipher, ByteArray key, ByteArrayAccess iv)
+        => proxy cipher -> key -> AEADMode -> iv -> Either StoreError (AEAD cipher)
+getAEAD cipher key mode iv = do
+    c <- getCipher cipher key
+    fromCryptoFailable $ aeadInit mode c iv
+
+authKeys :: ByteArrayAccess password
+         => password -> AuthEncParams
+         -> (B.ScrubbedBytes, B.ScrubbedBytes)
+authKeys key AuthEncParams{..} = (encKey, macKey)
+  where
+    encKDF = PBKDF2 "encryption" 1 Nothing prfAlgorithm
+    encLen = getMaximumKeySize encAlgorithm
+    encKey = kdfDerive encKDF encLen key
+
+    macKDF = PBKDF2 "authentication" 1 Nothing prfAlgorithm
+    macKey = kdfDerive macKDF macLen key
+
+    -- RFC 6476 section 4.2: "Specifying a MAC key size gets a bit tricky"
+    -- TODO: this is a hack but allows both test vectors to pass
+    macLen | encLen == 24 = 16
+           | otherwise    = getMaximumKeySize macAlgorithm
+
+checkAuthKey :: ByteArrayAccess cek => Int -> cek -> Either StoreError ()
+checkAuthKey sz key
+    | actual == sz = Right ()
+    | otherwise    = Left (CryptoError CryptoError_KeySizeInvalid)
+  where actual = B.length key
+
+ccpInit :: (ByteArrayAccess key, ByteArrayAccess aad)
+        => key
+        -> ChaChaPoly1305.Nonce
+        -> aad
+        -> Either StoreError ChaChaPoly1305.State
+ccpInit key nonce aad = case ChaChaPoly1305.initialize key nonce of
+    CryptoPassed s -> return (addAAD s)
+    CryptoFailed e -> Left (CryptoError e)
+  where addAAD = ChaChaPoly1305.finalizeAAD . ChaChaPoly1305.appendAAD aad
+
+ccpTag :: Poly1305.Auth -> AuthTag
+ccpTag (Poly1305.Auth bs) = AuthTag bs
+
+-- PRF
+
+-- | Pseudorandom function used for PBKDF2.
+data PBKDF2_PRF = PBKDF2_SHA1   -- ^ hmacWithSHA1
+                | PBKDF2_SHA256 -- ^ hmacWithSHA256
+                | PBKDF2_SHA512 -- ^ hmacWithSHA512
+                deriving (Show,Eq)
+
+instance Enumerable PBKDF2_PRF where
+    values = [ PBKDF2_SHA1
+             , PBKDF2_SHA256
+             , PBKDF2_SHA512
+             ]
+
+instance OIDable PBKDF2_PRF where
+    getObjectID PBKDF2_SHA1   = [1,2,840,113549,2,7]
+    getObjectID PBKDF2_SHA256 = [1,2,840,113549,2,9]
+    getObjectID PBKDF2_SHA512 = [1,2,840,113549,2,11]
+
+instance OIDNameable PBKDF2_PRF where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+instance AlgorithmId PBKDF2_PRF where
+    type AlgorithmType PBKDF2_PRF = PBKDF2_PRF
+    algorithmName _  = "PBKDF2 PRF"
+    algorithmType    = id
+    parameterASN1S _ = id
+    parseParameter p = getNextMaybe nullOrNothing >> return p
+
+-- | Invoke the pseudorandom function.
+prf :: (ByteArrayAccess salt, ByteArrayAccess password, ByteArray out)
+    => PBKDF2_PRF -> PBKDF2.Parameters -> password -> salt -> out
+prf PBKDF2_SHA1   = PBKDF2.fastPBKDF2_SHA1
+prf PBKDF2_SHA256 = PBKDF2.fastPBKDF2_SHA256
+prf PBKDF2_SHA512 = PBKDF2.fastPBKDF2_SHA512
+
+
+-- Key derivation
+
+-- | Salt value used for key derivation.
+type Salt = ByteString
+
+-- | Key derivation algorithm.
+data KeyDerivationAlgorithm = TypePBKDF2 | TypeScrypt
+
+instance Enumerable KeyDerivationAlgorithm where
+    values = [ TypePBKDF2
+             , TypeScrypt
+             ]
+
+instance OIDable KeyDerivationAlgorithm where
+    getObjectID TypePBKDF2 = [1,2,840,113549,1,5,12]
+    getObjectID TypeScrypt = [1,3,6,1,4,1,11591,4,11]
+
+instance OIDNameable KeyDerivationAlgorithm where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Key derivation algorithm and associated parameters.
+data KeyDerivationFunc =
+      -- | Key derivation with PBKDF2
+      PBKDF2 { pbkdf2Salt           :: Salt       -- ^ Salt value
+             , pbkdf2IterationCount :: Int        -- ^ Iteration count
+             , pbkdf2KeyLength      :: Maybe Int  -- ^ Optional key length
+             , pbkdf2Prf            :: PBKDF2_PRF -- ^ Pseudorandom function
+             }
+      -- | Key derivation with Scrypt
+    | Scrypt { scryptSalt      :: Salt       -- ^ Salt value
+             , scryptN         :: Word64     -- ^ N value
+             , scryptR         :: Int        -- ^ R value
+             , scryptP         :: Int        -- ^ P value
+             , scryptKeyLength :: Maybe Int  -- ^ Optional key length
+             }
+    deriving (Show,Eq)
+
+instance AlgorithmId KeyDerivationFunc where
+    type AlgorithmType KeyDerivationFunc = KeyDerivationAlgorithm
+
+    algorithmName _ = "key derivation algorithm"
+    algorithmType PBKDF2{..} = TypePBKDF2
+    algorithmType Scrypt{..} = TypeScrypt
+
+    parameterASN1S PBKDF2{..} =
+        asn1Container Sequence (salt . iters . keyLen . mprf)
+      where
+        salt   = gOctetString pbkdf2Salt
+        iters  = gIntVal (toInteger pbkdf2IterationCount)
+        keyLen = maybe id (gIntVal . toInteger) pbkdf2KeyLength
+        mprf   = if pbkdf2Prf == PBKDF2_SHA1 then id else algorithmASN1S Sequence pbkdf2Prf
+
+    parameterASN1S Scrypt{..} =
+        asn1Container Sequence (salt . n . r . p . keyLen)
+      where
+        salt   = gOctetString scryptSalt
+        n      = gIntVal (toInteger scryptN)
+        r      = gIntVal (toInteger scryptR)
+        p      = gIntVal (toInteger scryptP)
+        keyLen = maybe id (gIntVal . toInteger) scryptKeyLength
+
+    parseParameter TypePBKDF2 = onNextContainer Sequence $ do
+        OctetString salt <- getNext
+        IntVal iters <- getNext
+        keyLen <- getNextMaybe intOrNothing
+        b <- hasNext
+        mprf <- if b then parseAlgorithm Sequence else return PBKDF2_SHA1
+        return PBKDF2 { pbkdf2Salt           = salt
+                      , pbkdf2IterationCount = fromInteger iters
+                      , pbkdf2KeyLength      = fromInteger <$> keyLen
+                      , pbkdf2Prf            = mprf
+                      }
+
+    parseParameter TypeScrypt = onNextContainer Sequence $ do
+        OctetString salt <- getNext
+        IntVal n <- getNext
+        IntVal r <- getNext
+        IntVal p <- getNext
+        keyLen <- getNextMaybe intOrNothing
+        return Scrypt { scryptSalt      = salt
+                      , scryptN         = fromInteger n
+                      , scryptR         = fromInteger r
+                      , scryptP         = fromInteger p
+                      , scryptKeyLength = fromInteger <$> keyLen
+                      }
+
+-- | Return the optional key length stored in the KDF parameters.
+kdfKeyLength :: KeyDerivationFunc -> Maybe Int
+kdfKeyLength PBKDF2{..} = pbkdf2KeyLength
+kdfKeyLength Scrypt{..} = scryptKeyLength
+
+-- | Run a key derivation function to produce a result of the specified length
+-- using the supplied password.
+kdfDerive :: (ByteArrayAccess password, ByteArray out)
+          => KeyDerivationFunc -> Int -> password -> out
+kdfDerive PBKDF2{..} len pwd = prf pbkdf2Prf params pwd pbkdf2Salt
+  where params = PBKDF2.Parameters pbkdf2IterationCount len
+kdfDerive Scrypt{..} len pwd = Scrypt.generate params pwd scryptSalt
+  where params = Scrypt.Parameters { Scrypt.n = scryptN
+                                   , Scrypt.r = scryptR
+                                   , Scrypt.p = scryptP
+                                   , Scrypt.outputLength = len
+                                   }
+
+-- | Generate a random salt with the specified length in bytes.  To be most
+-- effective, the length should be at least 8 bytes.
+generateSalt :: MonadRandom m => Int -> m Salt
+generateSalt = getRandomBytes
+
+
+-- Key encryption
+
+data KeyEncryptionType = TypePWRIKEK
+                       | TypeAES128_WRAP
+                       | TypeAES192_WRAP
+                       | TypeAES256_WRAP
+                       | TypeAES128_WRAP_PAD
+                       | TypeAES192_WRAP_PAD
+                       | TypeAES256_WRAP_PAD
+                       | TypeDES_EDE3_WRAP
+                       | TypeRC2_WRAP
+
+instance Enumerable KeyEncryptionType where
+    values = [ TypePWRIKEK
+             , TypeAES128_WRAP
+             , TypeAES192_WRAP
+             , TypeAES256_WRAP
+             , TypeAES128_WRAP_PAD
+             , TypeAES192_WRAP_PAD
+             , TypeAES256_WRAP_PAD
+             , TypeDES_EDE3_WRAP
+             , TypeRC2_WRAP
+             ]
+
+instance OIDable KeyEncryptionType where
+    getObjectID TypePWRIKEK         = [1,2,840,113549,1,9,16,3,9]
+
+    getObjectID TypeAES128_WRAP     = [2,16,840,1,101,3,4,1,5]
+    getObjectID TypeAES192_WRAP     = [2,16,840,1,101,3,4,1,25]
+    getObjectID TypeAES256_WRAP     = [2,16,840,1,101,3,4,1,45]
+
+    getObjectID TypeAES128_WRAP_PAD = [2,16,840,1,101,3,4,1,8]
+    getObjectID TypeAES192_WRAP_PAD = [2,16,840,1,101,3,4,1,28]
+    getObjectID TypeAES256_WRAP_PAD = [2,16,840,1,101,3,4,1,48]
+
+    getObjectID TypeDES_EDE3_WRAP   = [1,2,840,113549,1,9,16,3,6]
+    getObjectID TypeRC2_WRAP        = [1,2,840,113549,1,9,16,3,7]
+
+instance OIDNameable KeyEncryptionType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Key encryption algorithm with associated parameters (i.e. the underlying
+-- encryption algorithm).
+data KeyEncryptionParams = PWRIKEK ContentEncryptionParams  -- ^ PWRI-KEK key wrap algorithm
+                         | AES128_WRAP                      -- ^ AES-128 key wrap
+                         | AES192_WRAP                      -- ^ AES-192 key wrap
+                         | AES256_WRAP                      -- ^ AES-256 key wrap
+                         | AES128_WRAP_PAD                  -- ^ AES-128 extended key wrap
+                         | AES192_WRAP_PAD                  -- ^ AES-192 extended key wrap
+                         | AES256_WRAP_PAD                  -- ^ AES-256 extended key wrap
+                         | DES_EDE3_WRAP                    -- ^ Triple-DES key wrap
+                         | RC2_WRAP Int                     -- ^ RC2 key wrap with effective key length
+                         deriving (Show,Eq)
+
+instance AlgorithmId KeyEncryptionParams where
+    type AlgorithmType KeyEncryptionParams = KeyEncryptionType
+    algorithmName _ = "key encryption algorithm"
+
+    algorithmType (PWRIKEK _)      = TypePWRIKEK
+    algorithmType AES128_WRAP      = TypeAES128_WRAP
+    algorithmType AES192_WRAP      = TypeAES192_WRAP
+    algorithmType AES256_WRAP      = TypeAES256_WRAP
+    algorithmType AES128_WRAP_PAD  = TypeAES128_WRAP_PAD
+    algorithmType AES192_WRAP_PAD  = TypeAES192_WRAP_PAD
+    algorithmType AES256_WRAP_PAD  = TypeAES256_WRAP_PAD
+    algorithmType DES_EDE3_WRAP    = TypeDES_EDE3_WRAP
+    algorithmType (RC2_WRAP _)     = TypeRC2_WRAP
+
+    parameterASN1S (PWRIKEK cep)  = asn1s cep
+    parameterASN1S DES_EDE3_WRAP  = gNull
+    parameterASN1S (RC2_WRAP ekl) = rc2VersionASN1 ekl
+    parameterASN1S _              = id
+
+    parseParameter TypePWRIKEK          = PWRIKEK <$> parse
+    parseParameter TypeAES128_WRAP      = return AES128_WRAP
+    parseParameter TypeAES192_WRAP      = return AES192_WRAP
+    parseParameter TypeAES256_WRAP      = return AES256_WRAP
+    parseParameter TypeAES128_WRAP_PAD  = return AES128_WRAP_PAD
+    parseParameter TypeAES192_WRAP_PAD  = return AES192_WRAP_PAD
+    parseParameter TypeAES256_WRAP_PAD  = return AES256_WRAP_PAD
+    parseParameter TypeDES_EDE3_WRAP    = getNextMaybe nullOrNothing >> return DES_EDE3_WRAP
+    parseParameter TypeRC2_WRAP         = RC2_WRAP <$> parseRC2Version
+
+instance HasKeySize KeyEncryptionParams where
+    getKeySizeSpecifier (PWRIKEK cep)   = getKeySizeSpecifier cep
+    getKeySizeSpecifier AES128_WRAP     = getCipherKeySizeSpecifier AES128
+    getKeySizeSpecifier AES192_WRAP     = getCipherKeySizeSpecifier AES192
+    getKeySizeSpecifier AES256_WRAP     = getCipherKeySizeSpecifier AES256
+    getKeySizeSpecifier AES128_WRAP_PAD = getCipherKeySizeSpecifier AES128
+    getKeySizeSpecifier AES192_WRAP_PAD = getCipherKeySizeSpecifier AES192
+    getKeySizeSpecifier AES256_WRAP_PAD = getCipherKeySizeSpecifier AES256
+    getKeySizeSpecifier DES_EDE3_WRAP   = getCipherKeySizeSpecifier DES_EDE3
+    getKeySizeSpecifier (RC2_WRAP _)    = KeySizeFixed 16
+
+-- | Encrypt a key with the specified key encryption key and algorithm.
+keyEncrypt :: (MonadRandom m, ByteArray kek, ByteArray ba)
+           => kek -> KeyEncryptionParams -> ba -> m (Either StoreError ba)
+keyEncrypt key (PWRIKEK params) bs =
+    case params of
+        ParamsECB cipher    -> let cc = getCipher cipher key in either (return . Left) (\c -> wrapEncrypt (const . ecbEncrypt) c undefined bs) cc
+        ParamsCBC cipher iv -> let cc = getCipher cipher key in either (return . Left) (\c -> wrapEncrypt cbcEncrypt c iv bs) cc
+        ParamsCBCRC2 len iv -> let cc = getRC2Cipher len key in either (return . Left) (\c -> wrapEncrypt cbcEncrypt c iv bs) cc
+        ParamsCFB cipher iv -> let cc = getCipher cipher key in either (return . Left) (\c -> wrapEncrypt cfbEncrypt c iv bs) cc
+        ParamsCTR _ _       -> return $ Left (InvalidParameter "Unable to wrap key in CTR mode")
+keyEncrypt key AES128_WRAP      bs = return (getCipher AES128 key >>= (`AES_KW.wrap` bs))
+keyEncrypt key AES192_WRAP      bs = return (getCipher AES192 key >>= (`AES_KW.wrap` bs))
+keyEncrypt key AES256_WRAP      bs = return (getCipher AES256 key >>= (`AES_KW.wrap` bs))
+keyEncrypt key AES128_WRAP_PAD  bs = return (getCipher AES128 key >>= (`AES_KW.wrapPad` bs))
+keyEncrypt key AES192_WRAP_PAD  bs = return (getCipher AES192 key >>= (`AES_KW.wrapPad` bs))
+keyEncrypt key AES256_WRAP_PAD  bs = return (getCipher AES256 key >>= (`AES_KW.wrapPad` bs))
+keyEncrypt key DES_EDE3_WRAP    bs = either (return . Left) (wrap3DES bs) (getCipher DES_EDE3 key)
+  where wrap3DES b c = (\iv -> TripleDES_KW.wrap c iv b) <$> ivGenerate c
+keyEncrypt key (RC2_WRAP ekl)   bs = either (return . Left) (wrapRC2 bs) (getRC2Cipher ekl key)
+  where wrapRC2 b c = do iv <- ivGenerate c; RC2_KW.wrap c iv b
+
+-- | Decrypt a key with the specified key encryption key and algorithm.
+keyDecrypt :: (ByteArray kek, ByteArray ba)
+           => kek -> KeyEncryptionParams -> ba -> Either StoreError ba
+keyDecrypt key (PWRIKEK params) bs =
+    case params of
+        ParamsECB cipher    -> getCipher cipher key >>= (\c -> wrapDecrypt (const . ecbDecrypt) c undefined bs)
+        ParamsCBC cipher iv -> getCipher cipher key >>= (\c -> wrapDecrypt cbcDecrypt c iv bs)
+        ParamsCBCRC2 len iv -> getRC2Cipher len key >>= (\c -> wrapDecrypt cbcDecrypt c iv bs)
+        ParamsCFB cipher iv -> getCipher cipher key >>= (\c -> wrapDecrypt cfbDecrypt c iv bs)
+        ParamsCTR _ _       -> Left (InvalidParameter "Unable to unwrap key in CTR mode")
+keyDecrypt key AES128_WRAP      bs = getCipher AES128   key >>= (`AES_KW.unwrap` bs)
+keyDecrypt key AES192_WRAP      bs = getCipher AES192   key >>= (`AES_KW.unwrap` bs)
+keyDecrypt key AES256_WRAP      bs = getCipher AES256   key >>= (`AES_KW.unwrap` bs)
+keyDecrypt key AES128_WRAP_PAD  bs = getCipher AES128   key >>= (`AES_KW.unwrapPad` bs)
+keyDecrypt key AES192_WRAP_PAD  bs = getCipher AES192   key >>= (`AES_KW.unwrapPad` bs)
+keyDecrypt key AES256_WRAP_PAD  bs = getCipher AES256   key >>= (`AES_KW.unwrapPad` bs)
+keyDecrypt key DES_EDE3_WRAP    bs = getCipher DES_EDE3 key >>= (`TripleDES_KW.unwrap` bs)
+keyDecrypt key (RC2_WRAP ekl)   bs = getRC2Cipher ekl key >>= (`RC2_KW.unwrap` bs)
+
+keyWrap :: (MonadRandom m, ByteArray ba)
+        => Int -> ba -> m (Either StoreError ba)
+keyWrap sz input
+    | inLen <   3 = return $ Left (InvalidInput "keyWrap: input key too short")
+    | inLen > 255 = return $ Left (InvalidInput "keyWrap: input key too long")
+    | pLen == 0   = return $ Right $ B.concat [ count, check, input ]
+    | otherwise   = do
+        padding <- getRandomBytes pLen
+        return $ Right $ B.concat [ count, check, input, padding ]
+  where
+    inLen = B.length input
+    count = B.singleton (fromIntegral inLen)
+    check = B.xor input (B.pack [255, 255, 255] :: B.Bytes)
+    pLen  = sz - (inLen + 4) `mod` sz + comp
+    comp  = if inLen + 4 > sz then 0 else sz
+
+keyUnwrap :: ByteArray ba => ba -> Either StoreError ba
+keyUnwrap input
+    | inLen < 4         = Left (InvalidInput "keyUnwrap: invalid wrapped key")
+    | valid             = Right $ B.take count (B.drop 4 input)
+    | otherwise         = Left (InvalidInput "keyUnwrap: invalid wrapped key")
+  where
+    inLen = B.length input
+    count = fromIntegral (B.index input 0)
+    bytes = [ B.index input (i + 1) `xor` B.index input (i + 4) | i <- [0..2] ]
+    valid = foldl1 (.&.) bytes == 0xFF &&! inLen >= count - 4
+
+wrapEncrypt :: (MonadRandom m, BlockCipher cipher, ByteArray ba)
+            => (cipher -> IV cipher -> ba -> ba)
+            -> cipher -> IV cipher -> ba -> m (Either StoreError ba)
+wrapEncrypt encFn cipher iv input = do
+    wrapped <- keyWrap sz input
+    return (fn <$> wrapped)
+  where
+    sz = blockSize cipher
+    fn formatted =
+        let firstPass = encFn cipher iv formatted
+            lastBlock = B.dropView firstPass (B.length firstPass - sz)
+            Just iv'  = makeIV lastBlock
+         in encFn cipher iv' firstPass
+
+wrapDecrypt :: (BlockCipher cipher, ByteArray ba)
+            => (cipher -> IV cipher -> ba -> ba)
+            -> cipher -> IV cipher -> ba -> Either StoreError ba
+wrapDecrypt decFn cipher iv input = keyUnwrap (decFn cipher iv firstPass)
+  where
+    sz = blockSize cipher
+    (beg, lb) = B.splitAt (B.length input - sz) input
+    lastBlock = decFn cipher iv' lb
+    Just iv'  = makeIV (B.dropView beg (B.length beg - sz))
+    Just iv'' = makeIV lastBlock
+    firstPass = decFn cipher iv'' beg `B.append` lastBlock
+
+
+-- Key transport
+
+-- | Encryption parameters for RSAES-OAEP.
+data OAEPParams = OAEPParams
+    { oaepHashAlgorithm :: DigestAlgorithm       -- ^ Hash function
+    , oaepMaskGenAlgorithm :: MaskGenerationFunc -- ^ Mask generation function
+    }
+    deriving (Show,Eq)
+
+withOAEPParams :: forall seed output a . (ByteArrayAccess seed, ByteArray output)
+               => OAEPParams
+               -> (forall hash . Hash.HashAlgorithm hash => RSAOAEP.OAEPParams hash seed output -> a)
+               -> a
+withOAEPParams p fn =
+    case oaepHashAlgorithm p of
+        DigestAlgorithm hashAlg ->
+            fn RSAOAEP.OAEPParams
+                { RSAOAEP.oaepHash = hashFromProxy hashAlg
+                , RSAOAEP.oaepMaskGenAlg = mgf (oaepMaskGenAlgorithm p)
+                , RSAOAEP.oaepLabel = Nothing
+                }
+
+instance ASN1Elem e => ProduceASN1Object e OAEPParams where
+    asn1s OAEPParams{..} =
+        asn1Container Sequence (h . m)
+      where
+        sha1  = DigestAlgorithm SHA1
+        tag i = asn1Container (Container Context i)
+
+        h | oaepHashAlgorithm == sha1 = id
+          | otherwise = tag 0 (algorithmASN1S Sequence oaepHashAlgorithm)
+
+        m | oaepMaskGenAlgorithm == MGF1 sha1 = id
+          | otherwise = tag 1 (algorithmASN1S Sequence oaepMaskGenAlgorithm)
+
+instance Monoid e => ParseASN1Object e OAEPParams where
+    parse = onNextContainer Sequence $ do
+        h <- tag 0 (parseAlgorithm Sequence)
+        m <- tag 1 (parseAlgorithm Sequence)
+        _ <- tag 2 parsePSpecified
+        return OAEPParams { oaepHashAlgorithm = fromMaybe sha1 h
+                          , oaepMaskGenAlgorithm = fromMaybe (MGF1 sha1) m
+                          }
+      where
+        sha1  = DigestAlgorithm SHA1
+        tag i = onNextContainerMaybe (Container Context i)
+
+        parsePSpecified = do
+            OID [1,2,840,113549,1,1,9] <- getNext
+            OctetString p <- getNext
+            guard (B.null p)
+
+data KeyTransportType = TypeRSAES
+                      | TypeRSAESOAEP
+
+instance Enumerable KeyTransportType where
+    values = [ TypeRSAES
+             , TypeRSAESOAEP
+             ]
+
+instance OIDable KeyTransportType where
+    getObjectID TypeRSAES          = [1,2,840,113549,1,1,1]
+    getObjectID TypeRSAESOAEP      = [1,2,840,113549,1,1,7]
+
+instance OIDNameable KeyTransportType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Key transport algorithm with associated parameters.
+data KeyTransportParams = RSAES                 -- ^ RSAES-PKCS1
+                        | RSAESOAEP OAEPParams  -- ^ RSAES-OAEP
+                        deriving (Show,Eq)
+
+instance AlgorithmId KeyTransportParams where
+    type AlgorithmType KeyTransportParams = KeyTransportType
+    algorithmName _ = "key transport algorithm"
+
+    algorithmType RSAES              = TypeRSAES
+    algorithmType (RSAESOAEP _)      = TypeRSAESOAEP
+
+    parameterASN1S RSAES             = gNull
+    parameterASN1S (RSAESOAEP p)     = asn1s p
+
+    parseParameter TypeRSAES         = getNextMaybe nullOrNothing >> return RSAES
+    parseParameter TypeRSAESOAEP     = RSAESOAEP <$> parse
+
+-- | Encrypt the specified content with a key-transport algorithm and recipient
+-- public key.
+transportEncrypt :: MonadRandom m
+                 => KeyTransportParams
+                 -> X509.PubKey
+                 -> ByteString
+                 -> m (Either StoreError ByteString)
+transportEncrypt RSAES         (X509.PubKeyRSA pub) bs =
+    mapLeft RSAError <$> RSA.encrypt pub bs
+transportEncrypt (RSAESOAEP p) (X509.PubKeyRSA pub) bs =
+    withOAEPParams p $ \params ->
+        mapLeft RSAError <$> RSAOAEP.encrypt params pub bs
+transportEncrypt _ _ _ = return $ Left UnexpectedPublicKeyType
+
+-- | Decrypt the specified content with a key-transport algorithm and recipient
+-- private key.
+transportDecrypt :: MonadRandom m
+                 => KeyTransportParams
+                 -> X509.PrivKey
+                 -> ByteString
+                 -> m (Either StoreError ByteString)
+transportDecrypt RSAES         (X509.PrivKeyRSA priv) bs =
+    mapLeft RSAError <$> RSA.decryptSafer priv bs
+transportDecrypt (RSAESOAEP p) (X509.PrivKeyRSA priv) bs =
+    withOAEPParams p $ \params ->
+        mapLeft RSAError <$> RSAOAEP.decryptSafer params priv bs
+transportDecrypt _ _ _ = return $ Left UnexpectedPrivateKeyType
+
+
+-- Key agreement
+
+data KeyAgreementType = TypeStdDH DigestAlgorithm
+                      | TypeCofactorDH DigestAlgorithm
+                      deriving (Show,Eq)
+
+instance Enumerable KeyAgreementType where
+    values = [ TypeStdDH (DigestAlgorithm SHA1)
+             , TypeStdDH (DigestAlgorithm SHA224)
+             , TypeStdDH (DigestAlgorithm SHA256)
+             , TypeStdDH (DigestAlgorithm SHA384)
+             , TypeStdDH (DigestAlgorithm SHA512)
+
+             , TypeCofactorDH (DigestAlgorithm SHA1)
+             , TypeCofactorDH (DigestAlgorithm SHA224)
+             , TypeCofactorDH (DigestAlgorithm SHA256)
+             , TypeCofactorDH (DigestAlgorithm SHA384)
+             , TypeCofactorDH (DigestAlgorithm SHA512)
+             ]
+
+instance OIDable KeyAgreementType where
+    getObjectID (TypeStdDH (DigestAlgorithm SHA1))        = [1,3,133,16,840,63,0,2]
+    getObjectID (TypeStdDH (DigestAlgorithm SHA224))      = [1,3,132,1,11,0]
+    getObjectID (TypeStdDH (DigestAlgorithm SHA256))      = [1,3,132,1,11,1]
+    getObjectID (TypeStdDH (DigestAlgorithm SHA384))      = [1,3,132,1,11,2]
+    getObjectID (TypeStdDH (DigestAlgorithm SHA512))      = [1,3,132,1,11,3]
+
+    getObjectID (TypeCofactorDH (DigestAlgorithm SHA1))   = [1,3,133,16,840,63,0,3]
+    getObjectID (TypeCofactorDH (DigestAlgorithm SHA224)) = [1,3,132,1,14,0]
+    getObjectID (TypeCofactorDH (DigestAlgorithm SHA256)) = [1,3,132,1,14,1]
+    getObjectID (TypeCofactorDH (DigestAlgorithm SHA384)) = [1,3,132,1,14,2]
+    getObjectID (TypeCofactorDH (DigestAlgorithm SHA512)) = [1,3,132,1,14,3]
+
+    getObjectID ty = error ("Unsupported KeyAgreementType: " ++ show ty)
+
+instance OIDNameable KeyAgreementType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Key agreement algorithm with associated parameters.
+data KeyAgreementParams = StdDH DigestAlgorithm KeyEncryptionParams
+                          -- ^ 1-Pass D-H with Stardard ECDH
+                        | CofactorDH DigestAlgorithm KeyEncryptionParams
+                          -- ^ 1-Pass D-H with Cofactor ECDH
+                        deriving (Show,Eq)
+
+instance AlgorithmId KeyAgreementParams where
+    type AlgorithmType KeyAgreementParams = KeyAgreementType
+    algorithmName _ = "key agreement algorithm"
+
+    algorithmType (StdDH d _)         = TypeStdDH d
+    algorithmType (CofactorDH d _)    = TypeCofactorDH d
+
+    parameterASN1S (StdDH _ p)        = algorithmASN1S Sequence p
+    parameterASN1S (CofactorDH _ p)   = algorithmASN1S Sequence p
+
+    parseParameter (TypeStdDH d)      = StdDH d <$> parseAlgorithm Sequence
+    parseParameter (TypeCofactorDH d) = CofactorDH d <$> parseAlgorithm Sequence
+
+ecdhKeyMaterial :: (ByteArrayAccess bin, ByteArray bout)
+                => DigestAlgorithm -> KeyEncryptionParams -> Maybe ByteString -> bin -> bout
+ecdhKeyMaterial (DigestAlgorithm hashAlg) kep ukm zz
+    | r == 0    = B.concat (map chunk [1..d])
+    | otherwise = B.concat (map chunk [1..d]) `B.append` B.take r (chunk $ succ d)
+  where
+    (d, r)   = outLen `divMod` Hash.hashDigestSize prx
+
+    prx      = hashFromProxy hashAlg
+    outLen   = getMaximumKeySize kep
+    outBits  = 8 * outLen
+    toWord32 = i2ospOf_ 4 . fromIntegral
+
+    chunk     = B.convert . Hash.hashFinalize . hashCtx
+    hashCtx'  = Hash.hashInitWith prx
+    hashCtx i = Hash.hashUpdate (Hash.hashUpdate (Hash.hashUpdate hashCtx' zz) (toWord32 i)) otherInfo
+    otherInfo =
+        let ki  = algorithmASN1S Sequence kep
+            eui = case ukm of
+                    Nothing -> id
+                    Just bs -> asn1Container (Container Context 0)
+                                   (gOctetString bs)
+            spi = asn1Container (Container Context 2)
+                      (gOctetString $ toWord32 outBits)
+         in encodeASN1S $ asn1Container Sequence (ki . eui . spi)
+
+-- | Generate an ephemeral ECDH key.
+ecdhGenerate :: MonadRandom m => X509.PubKey -> m (Either StoreError (ECC.Curve, ECC.PrivateNumber, X509.SerializedPoint))
+ecdhGenerate (X509.PubKeyEC pub) =
+    case ecPubKeyCurveName pub of
+        Nothing -> return $ Left NamedCurveRequired
+        Just n  -> do
+            let curve = ECC.getCurveByName n
+            priv <- ECDH.generatePrivate curve
+            return $ Right (curve, priv, X509.pubkeyEC_pub pub)
+ecdhGenerate _ = return $ Left UnexpectedPublicKeyType
+
+-- | Encrypt the specified content with an ECDH key pair and key-agreement
+-- algorithm.
+ecdhEncrypt :: (MonadRandom m, ByteArray ba)
+            => KeyAgreementParams -> Maybe ByteString -> ECC.Curve -> ECC.PrivateNumber -> X509.SerializedPoint -> ba -> m (Either StoreError ba)
+ecdhEncrypt (StdDH dig kep) ukm curve d pt bs =
+    case unserializePoint curve pt of
+        Nothing  -> return $ Left (InvalidInput "Invalid serialized point")
+        Just pub -> do
+            let s = ECDH.getShared curve d pub
+                k = ecdhKeyMaterial dig kep ukm s :: B.ScrubbedBytes
+            keyEncrypt k kep bs
+ecdhEncrypt (CofactorDH dig kep) ukm curve d pt bs =
+    case unserializePoint curve pt of
+        Nothing  -> return $ Left (InvalidInput "Invalid serialized point")
+        Just pub -> do
+            let h = ECC.ecc_h (ECC.common_curve curve)
+                s = ECDH.getShared curve (h * d) pub
+                k = ecdhKeyMaterial dig kep ukm s :: B.ScrubbedBytes
+            keyEncrypt k kep bs
+
+-- | Decrypt the specified content with an ECDH key pair and key-agreement
+-- algorithm.
+ecdhDecrypt :: ByteArray ba
+            => KeyAgreementParams -> Maybe ByteString -> X509.PrivKeyEC -> X509.SerializedPoint -> ba -> Either StoreError ba
+ecdhDecrypt (StdDH dig kep) ukm priv pt bs =
+    case ecPrivKeyCurve priv of
+        Nothing    -> Left UnsupportedEllipticCurve
+        Just curve ->
+            case unserializePoint curve pt of
+                Nothing  -> Left (InvalidInput "Invalid serialized point")
+                Just pub -> do
+                    let d = X509.privkeyEC_priv priv
+                        s = ECDH.getShared curve d pub
+                        k = ecdhKeyMaterial dig kep ukm s :: B.ScrubbedBytes
+                    keyDecrypt k kep bs
+ecdhDecrypt (CofactorDH dig kep) ukm priv pt bs =
+    case ecPrivKeyCurve priv of
+        Nothing    -> Left UnsupportedEllipticCurve
+        Just curve ->
+            case unserializePoint curve pt of
+                Nothing  -> Left (InvalidInput "Invalid serialized point")
+                Just pub -> do
+                    let h = ECC.ecc_h (ECC.common_curve curve)
+                        d = X509.privkeyEC_priv priv
+                        s = ECDH.getShared curve (h * d) pub
+                        k = ecdhKeyMaterial dig kep ukm s :: B.ScrubbedBytes
+                    keyDecrypt k kep bs
+
+
+-- Utilities
+
+getCipher :: (BlockCipher cipher, ByteArray key)
+          => proxy cipher -> key -> Either StoreError cipher
+getCipher _ key = fromCryptoFailable (cipherInit key)
+
+getRC2Cipher :: ByteArray key => Int -> key -> Either StoreError RC2
+getRC2Cipher len key = fromCryptoFailable (rc2WithEffectiveKeyLength len key)
+
+ivGenerate :: (BlockCipher cipher, MonadRandom m) => cipher -> m (IV cipher)
+ivGenerate cipher = do
+    bs <- getRandomBytes (blockSize cipher)
+    let Just iv = makeIV (bs :: ByteString)
+    return iv
+
+nonceGenerate :: MonadRandom m => Int -> m B.Bytes
+nonceGenerate = getRandomBytes
+
+cipherFromProxy :: proxy cipher -> cipher
+cipherFromProxy _ = undefined
+
+-- | Return the block size of the specified block cipher.
+proxyBlockSize :: BlockCipher cipher => proxy cipher -> Int
+proxyBlockSize = blockSize . cipherFromProxy
+
+getL :: CCM_L -> Int
+getL CCM_L2 = 2
+getL CCM_L3 = 3
+getL CCM_L4 = 4
+
+getM :: CCM_M -> Int
+getM CCM_M4  = 4
+getM CCM_M6  = 6
+getM CCM_M8  = 8
+getM CCM_M10 = 10
+getM CCM_M12 = 12
+getM CCM_M14 = 14
+getM CCM_M16 = 16
+
+fromL :: Int -> Maybe CCM_L
+fromL 2 = Just CCM_L2
+fromL 3 = Just CCM_L3
+fromL 4 = Just CCM_L4
+fromL _ = Nothing
+
+parseM :: Monoid e => ParseASN1 e CCM_M
+parseM = do
+    IntVal l <- getNext
+    case l of
+        4  -> return CCM_M4
+        6  -> return CCM_M6
+        8  -> return CCM_M8
+        10 -> return CCM_M10
+        12 -> return CCM_M12
+        14 -> return CCM_M14
+        16 -> return CCM_M16
+        i -> throwParseError ("Parsed invalid CCM parameter M: " ++ show i)
+
+
+-- Mask generation functions
+
+data MaskGenerationType = TypeMGF1
+    deriving (Show,Eq)
+
+instance Enumerable MaskGenerationType where
+    values = [ TypeMGF1
+             ]
+
+instance OIDable MaskGenerationType where
+    getObjectID TypeMGF1     = [1,2,840,113549,1,1,8]
+
+instance OIDNameable MaskGenerationType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Mask Generation Functions (MGF) and associated parameters.
+newtype MaskGenerationFunc = MGF1 DigestAlgorithm
+    deriving (Show,Eq)
+
+instance AlgorithmId MaskGenerationFunc where
+    type AlgorithmType MaskGenerationFunc = MaskGenerationType
+    algorithmName _  = "mask generation function"
+
+    algorithmType (MGF1 _)   = TypeMGF1
+
+    parameterASN1S (MGF1 d)  = algorithmASN1S Sequence d
+
+    parseParameter TypeMGF1  = MGF1 <$> parseAlgorithm Sequence
+
+-- | Generate a mask with the MGF.
+mgf :: (ByteArrayAccess seed, ByteArray output)
+    => MaskGenerationFunc -> seed -> Int -> output
+mgf (MGF1 (DigestAlgorithm hashAlg)) = MGF.mgf1 (hashFromProxy hashAlg)
+
+
+-- Signature algorithms
+
+-- | Signature value.
+type SignatureValue = ByteString
+
+-- | Signature parameters for RSASSA-PSS.
+data PSSParams = PSSParams
+    { pssHashAlgorithm :: DigestAlgorithm       -- ^ Hash function
+    , pssMaskGenAlgorithm :: MaskGenerationFunc -- ^ Mask generation function
+    , pssSaltLength :: Int                      -- ^ Length of the salt in bytes
+    }
+    deriving (Show,Eq)
+
+withPSSParams :: forall seed output a . (ByteArrayAccess seed, ByteArray output)
+              => PSSParams
+              -> (forall hash . Hash.HashAlgorithm hash => RSAPSS.PSSParams hash seed output -> a)
+              -> a
+withPSSParams p fn =
+    case pssHashAlgorithm p of
+        DigestAlgorithm hashAlg ->
+            fn RSAPSS.PSSParams
+                { RSAPSS.pssHash = hashFromProxy hashAlg
+                , RSAPSS.pssMaskGenAlg = mgf (pssMaskGenAlgorithm p)
+                , RSAPSS.pssSaltLength = pssSaltLength p
+                , RSAPSS.pssTrailerField = 0xbc
+                }
+
+instance ASN1Elem e => ProduceASN1Object e PSSParams where
+    asn1s PSSParams{..} =
+        asn1Container Sequence (h . m . s)
+      where
+        sha1  = DigestAlgorithm SHA1
+        tag i = asn1Container (Container Context i)
+
+        h | pssHashAlgorithm == sha1 = id
+          | otherwise = tag 0 (algorithmASN1S Sequence pssHashAlgorithm)
+
+        m | pssMaskGenAlgorithm == MGF1 sha1 = id
+          | otherwise = tag 1 (algorithmASN1S Sequence pssMaskGenAlgorithm)
+
+        s | pssSaltLength == 20 && pssHashAlgorithm == sha1 = id
+          | otherwise = tag 2 (gIntVal $ fromIntegral pssSaltLength)
+
+instance Monoid e => ParseASN1Object e PSSParams where
+    parse = onNextContainer Sequence $ do
+        h <- tag 0 (parseAlgorithm Sequence)
+        m <- tag 1 (parseAlgorithm Sequence)
+        s <- tag 2 $ do { IntVal i <- getNext; return (fromIntegral i) }
+        _ <- tag 3 $ do { IntVal 1 <- getNext; return () }
+        return PSSParams { pssHashAlgorithm = fromMaybe sha1 h
+                         , pssMaskGenAlgorithm = fromMaybe (MGF1 sha1) m
+                         , pssSaltLength = fromMaybe 20 s
+                         }
+      where
+        sha1  = DigestAlgorithm SHA1
+        tag i = onNextContainerMaybe (Container Context i)
+
+data SignatureType = TypeRSAAnyHash
+                   | TypeRSA DigestAlgorithm
+                   | TypeRSAPSS
+                   | TypeDSA DigestAlgorithm
+                   | TypeECDSA DigestAlgorithm
+    deriving (Show,Eq)
+
+instance Enumerable SignatureType where
+    values = [ TypeRSAAnyHash
+
+             , TypeRSA (DigestAlgorithm MD2)
+             , TypeRSA (DigestAlgorithm MD5)
+             , TypeRSA (DigestAlgorithm SHA1)
+             , TypeRSA (DigestAlgorithm SHA224)
+             , TypeRSA (DigestAlgorithm SHA256)
+             , TypeRSA (DigestAlgorithm SHA384)
+             , TypeRSA (DigestAlgorithm SHA512)
+
+             , TypeRSAPSS
+
+             , TypeDSA (DigestAlgorithm SHA1)
+             , TypeDSA (DigestAlgorithm SHA224)
+             , TypeDSA (DigestAlgorithm SHA256)
+
+             , TypeECDSA (DigestAlgorithm SHA1)
+             , TypeECDSA (DigestAlgorithm SHA224)
+             , TypeECDSA (DigestAlgorithm SHA256)
+             , TypeECDSA (DigestAlgorithm SHA384)
+             , TypeECDSA (DigestAlgorithm SHA512)
+             ]
+
+instance OIDable SignatureType where
+    getObjectID TypeRSAAnyHash                       = [1,2,840,113549,1,1,1]
+
+    getObjectID (TypeRSA (DigestAlgorithm MD2))      = [1,2,840,113549,1,1,2]
+    getObjectID (TypeRSA (DigestAlgorithm MD5))      = [1,2,840,113549,1,1,4]
+    getObjectID (TypeRSA (DigestAlgorithm SHA1))     = [1,2,840,113549,1,1,5]
+    getObjectID (TypeRSA (DigestAlgorithm SHA224))   = [1,2,840,113549,1,1,14]
+    getObjectID (TypeRSA (DigestAlgorithm SHA256))   = [1,2,840,113549,1,1,11]
+    getObjectID (TypeRSA (DigestAlgorithm SHA384))   = [1,2,840,113549,1,1,12]
+    getObjectID (TypeRSA (DigestAlgorithm SHA512))   = [1,2,840,113549,1,1,13]
+
+    getObjectID TypeRSAPSS                           = [1,2,840,113549,1,1,10]
+
+    getObjectID (TypeDSA (DigestAlgorithm SHA1))     = [1,2,840,10040,4,3]
+    getObjectID (TypeDSA (DigestAlgorithm SHA224))   = [2,16,840,1,101,3,4,3,1]
+    getObjectID (TypeDSA (DigestAlgorithm SHA256))   = [2,16,840,1,101,3,4,3,2]
+
+    getObjectID (TypeECDSA (DigestAlgorithm SHA1))   = [1,2,840,10045,4,1]
+    getObjectID (TypeECDSA (DigestAlgorithm SHA224)) = [1,2,840,10045,4,3,1]
+    getObjectID (TypeECDSA (DigestAlgorithm SHA256)) = [1,2,840,10045,4,3,2]
+    getObjectID (TypeECDSA (DigestAlgorithm SHA384)) = [1,2,840,10045,4,3,3]
+    getObjectID (TypeECDSA (DigestAlgorithm SHA512)) = [1,2,840,10045,4,3,4]
+
+    getObjectID ty = error ("Unsupported SignatureType: " ++ show ty)
+
+instance OIDNameable SignatureType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | CMS signature algorithms and associated parameters.
+data SignatureAlg = RSAAnyHash
+                  | RSA DigestAlgorithm
+                  | RSAPSS PSSParams
+                  | DSA DigestAlgorithm
+                  | ECDSA DigestAlgorithm
+    deriving (Show,Eq)
+
+instance AlgorithmId SignatureAlg where
+    type AlgorithmType SignatureAlg = SignatureType
+    algorithmName _  = "signature algorithm"
+
+    algorithmType RSAAnyHash  = TypeRSAAnyHash
+    algorithmType (RSA alg)   = TypeRSA alg
+    algorithmType (RSAPSS _)  = TypeRSAPSS
+    algorithmType (DSA alg)   = TypeDSA alg
+    algorithmType (ECDSA alg) = TypeECDSA alg
+
+    parameterASN1S RSAAnyHash = gNull
+    parameterASN1S (RSA _)    = gNull
+    parameterASN1S (RSAPSS p) = asn1s p
+    parameterASN1S (DSA _)    = id
+    parameterASN1S (ECDSA _)  = id
+
+    parseParameter TypeRSAAnyHash   = getNextMaybe nullOrNothing >> return RSAAnyHash
+    parseParameter (TypeRSA alg)    = getNextMaybe nullOrNothing >> return (RSA alg)
+    parseParameter TypeRSAPSS       = RSAPSS <$> parse
+    parseParameter (TypeDSA alg)    = return (DSA alg)
+    parseParameter (TypeECDSA alg)  = return (ECDSA alg)
+
+-- | Sign a message using the specified algorithm and private key.
+signatureGenerate :: MonadRandom m => SignatureAlg -> X509.PrivKey -> ByteString -> m (Either StoreError SignatureValue)
+signatureGenerate RSAAnyHash _ _ =
+    error "signatureGenerate: should call signatureResolveHash first"
+signatureGenerate (RSA alg)   (X509.PrivKeyRSA priv) msg =
+    let err = return . Left $ InvalidParameter ("Invalid hash algorithm for RSA: " ++ show alg)
+     in withHashAlgorithmASN1 alg err $ \hashAlg ->
+            mapLeft RSAError <$> RSA.signSafer (Just hashAlg) priv msg
+signatureGenerate (RSAPSS p)  (X509.PrivKeyRSA priv) msg =
+    withPSSParams p $ \params ->
+        mapLeft RSAError <$> RSAPSS.signSafer params priv msg
+signatureGenerate (DSA alg)   (X509.PrivKeyDSA priv) msg =
+    case alg of
+        DigestAlgorithm t ->
+            Right . dsaFromSignature <$> DSA.sign priv (hashFromProxy t) msg
+signatureGenerate (ECDSA alg) (X509.PrivKeyEC priv)  msg =
+    case alg of
+        DigestAlgorithm t ->
+            case ecdsaToPrivateKey priv of
+                Nothing -> return (Left UnsupportedEllipticCurve)
+                Just p  ->
+                    let h = hashFromProxy t
+                     in Right . ecdsaFromSignature <$> ECDSA.sign p h msg
+signatureGenerate _ _ _ = return (Left UnexpectedPrivateKeyType)
+
+-- | Verify a message signature using the specified algorithm and public key.
+signatureVerify :: SignatureAlg -> X509.PubKey -> ByteString -> SignatureValue -> Bool
+signatureVerify RSAAnyHash _ _ _ =
+    error "signatureVerify: should call signatureResolveHash first"
+signatureVerify (RSA alg)   (X509.PubKeyRSA pub) msg sig =
+    withHashAlgorithmASN1 alg False $ \hashAlg ->
+        RSA.verify (Just hashAlg) pub msg sig
+signatureVerify (RSAPSS p)  (X509.PubKeyRSA pub) msg sig =
+    withPSSParams p $ \params -> RSAPSS.verify params pub msg sig
+signatureVerify (DSA alg)   (X509.PubKeyDSA pub) msg sig = fromMaybe False $ do
+    s <- dsaToSignature sig
+    case alg of
+        DigestAlgorithm t -> return $ DSA.verify (hashFromProxy t) pub s msg
+signatureVerify (ECDSA alg) (X509.PubKeyEC pub)  msg sig = fromMaybe False $ do
+    p <- ecdsaToPublicKey pub
+    s <- ecdsaToSignature sig
+    case alg of
+        DigestAlgorithm t -> return $ ECDSA.verify (hashFromProxy t) p s msg
+signatureVerify _                 _                    _   _   = False
+
+withHashAlgorithmASN1 :: DigestAlgorithm
+                      -> a
+                      -> (forall hashAlg . RSA.HashAlgorithmASN1 hashAlg => hashAlg -> a)
+                      -> a
+withHashAlgorithmASN1 (DigestAlgorithm MD2)    _ f = f Hash.MD2
+withHashAlgorithmASN1 (DigestAlgorithm MD5)    _ f = f Hash.MD5
+withHashAlgorithmASN1 (DigestAlgorithm SHA1)   _ f = f Hash.SHA1
+withHashAlgorithmASN1 (DigestAlgorithm SHA224) _ f = f Hash.SHA224
+withHashAlgorithmASN1 (DigestAlgorithm SHA256) _ f = f Hash.SHA256
+withHashAlgorithmASN1 (DigestAlgorithm SHA384) _ f = f Hash.SHA384
+withHashAlgorithmASN1 (DigestAlgorithm SHA512) _ f = f Hash.SHA512
+withHashAlgorithmASN1 _                        e _ = e
+
+-- | Return on which digest algorithm the specified signature algorithm is
+-- based, as well as a substitution algorithm for when a default digest
+-- algorithm is required.
+signatureResolveHash :: DigestAlgorithm -> SignatureAlg -> (DigestAlgorithm, SignatureAlg)
+signatureResolveHash d RSAAnyHash     = (d, RSA d)
+signatureResolveHash _ alg@(RSA d)    = (d, alg)
+signatureResolveHash _ alg@(RSAPSS p) = (pssHashAlgorithm p, alg)
+signatureResolveHash _ alg@(DSA d)    = (d, alg)
+signatureResolveHash _ alg@(ECDSA d)  = (d, alg)
+
+-- | Check that a signature algorithm is based on the specified digest algorithm
+-- and return a substitution algorithm for when a default digest algorithm is
+-- required.
+signatureCheckHash :: DigestAlgorithm -> SignatureAlg -> Maybe SignatureAlg
+signatureCheckHash expected RSAAnyHash = Just $ RSA expected
+signatureCheckHash expected alg@(RSA found)
+    | expected == found = Just alg
+    | otherwise         = Nothing
+signatureCheckHash expected alg@(RSAPSS p)
+    | expected == pssHashAlgorithm p = Just alg
+    | otherwise                      = Nothing
+signatureCheckHash expected alg@(DSA found)
+    | expected == found = Just alg
+    | otherwise         = Nothing
+signatureCheckHash expected alg@(ECDSA found)
+    | expected == found = Just alg
+    | otherwise         = Nothing
+
+dsaToSignature :: ByteString -> Maybe DSA.Signature
+dsaToSignature b = tryDecodeAndParse b $ onNextContainer Sequence $ do
+    IntVal r <- getNext
+    IntVal s <- getNext
+    return DSA.Signature { DSA.sign_r = r, DSA.sign_s = s }
+
+dsaFromSignature :: DSA.Signature -> ByteString
+dsaFromSignature sig = encodeASN1S $ asn1Container Sequence
+    (gIntVal (DSA.sign_r sig) . gIntVal (DSA.sign_s sig))
+
+ecdsaToSignature :: ByteString -> Maybe ECDSA.Signature
+ecdsaToSignature b = tryDecodeAndParse b $ onNextContainer Sequence $ do
+    IntVal r <- getNext
+    IntVal s <- getNext
+    return ECDSA.Signature { ECDSA.sign_r = r, ECDSA.sign_s = s }
+
+ecdsaFromSignature :: ECDSA.Signature -> ByteString
+ecdsaFromSignature sig = encodeASN1S $ asn1Container Sequence
+    (gIntVal (ECDSA.sign_r sig) . gIntVal (ECDSA.sign_s sig))
+
+ecdsaToPublicKey :: X509.PubKeyEC -> Maybe ECDSA.PublicKey
+ecdsaToPublicKey key = do
+    curve <- ecPubKeyCurve key
+    pt <- unserializePoint curve (X509.pubkeyEC_pub key)
+    return ECDSA.PublicKey { ECDSA.public_curve = curve, ECDSA.public_q = pt }
+
+ecdsaToPrivateKey :: X509.PrivKeyEC -> Maybe ECDSA.PrivateKey
+ecdsaToPrivateKey key = do
+    curve <- ecPrivKeyCurve key
+    let d = X509.privkeyEC_priv key
+    return ECDSA.PrivateKey { ECDSA.private_curve = curve, ECDSA.private_d = d }
+
+tryDecodeAndParse :: ByteString -> ParseASN1 () a -> Maybe a
+tryDecodeAndParse b parser =
+    either (const Nothing) Just $
+        case decodeASN1' BER b of
+            Left _     -> Left undefined -- value ignored
+            Right asn1 -> runParseASN1 parser asn1
diff --git a/src/Crypto/Store/CMS/Attribute.hs b/src/Crypto/Store/CMS/Attribute.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Attribute.hs
@@ -0,0 +1,126 @@
+-- |
+-- Module      : Crypto.Store.CMS.Attribute
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- CMS attributes
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.Attribute
+    ( Attribute(..)
+    , attributesASN1S
+    , parseAttributes
+    -- * Generic attribute
+    , findAttribute
+    , setAttribute
+    , filterAttributes
+    -- * Implementing attributes
+    , setAttributeASN1S
+    , runParseAttribute
+    -- * Standard attributes
+    , getContentTypeAttr
+    , setContentTypeAttr
+    , getMessageDigestAttr
+    , setMessageDigestAttr
+    ) where
+
+import Data.ASN1.Types
+import Data.ByteString (ByteString)
+import Data.Maybe (fromMaybe)
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+
+-- | An attribute extending the parent structure with arbitrary data.
+data Attribute = Attribute
+    { attrType   :: OID    -- ^ Attribute type
+    , attrValues :: [ASN1] -- ^ Attribute values
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e Attribute where
+    asn1s Attribute{..} =
+        asn1Container Sequence
+            (gOID attrType . asn1Container Set (gMany attrValues))
+
+instance Monoid e => ParseASN1Object e Attribute where
+    parse = onNextContainer Sequence $ do
+        OID oid <- getNext
+        vals <- onNextContainer Set (getMany getNext)
+        return Attribute { attrType = oid, attrValues = vals }
+
+-- | Produce the ASN.1 stream for a list of attributes.
+attributesASN1S :: ASN1Elem e
+                => ASN1ConstructionType -> [Attribute] -> ASN1Stream e
+attributesASN1S _  []    = id
+attributesASN1S ty attrs = asn1Container ty (asn1s attrs)
+
+-- | Parse a list of attributes.
+parseAttributes :: Monoid e => ASN1ConstructionType -> ParseASN1 e [Attribute]
+parseAttributes ty = fromMaybe [] <$> onNextContainerMaybe ty parse
+
+-- | Return the values for the first attribute with the specified type.
+findAttribute :: OID -> [Attribute] -> Maybe [ASN1]
+findAttribute oid attrs =
+    case [ attrValues a | a <- attrs, attrType a == oid ] of
+        []    -> Nothing
+        (v:_) -> Just v
+
+-- | Filter a list of attributes based on a predicate applied to attribute type.
+filterAttributes :: (OID -> Bool) -> [Attribute] -> [Attribute]
+filterAttributes p = filter (p . attrType)
+
+-- | Add or replace an attribute in a list of attributes.
+setAttribute :: OID -> [ASN1] -> [Attribute] -> [Attribute]
+setAttribute oid vals = (:) attr . filterAttributes (/= oid)
+  where attr = Attribute { attrType = oid, attrValues = vals }
+
+-- | Find an attribute with the specified attribute and run a parser on the
+-- attribute value when found.  'Nothing' is returned if the attribute could not
+-- be found but also when the parse failed.
+runParseAttribute :: OID -> [Attribute] -> ParseASN1 () a -> Maybe a
+runParseAttribute oid attrs p =
+    case findAttribute oid attrs of
+        Nothing -> Nothing
+        Just s  -> either (const Nothing) Just (runParseASN1 p s)
+
+-- | Add or replace an attribute in a list of attributes, using 'ASN1S'.
+setAttributeASN1S :: OID -> ASN1S -> [Attribute] -> [Attribute]
+setAttributeASN1S oid g = setAttribute oid (g [])
+
+
+-- Content type
+
+contentType :: OID
+contentType = [1,2,840,113549,1,9,3]
+
+-- | Return the value of the @contentType@ attribute.
+getContentTypeAttr :: [Attribute] -> Maybe ContentType
+getContentTypeAttr attrs = runParseAttribute contentType attrs $ do
+    OID oid <- getNext
+    withObjectID "content type" oid return
+
+-- | Add or replace the @contentType@ attribute in a list of attributes.
+setContentTypeAttr :: ContentType -> [Attribute] -> [Attribute]
+setContentTypeAttr ct = setAttributeASN1S contentType (gOID $ getObjectID ct)
+
+
+-- Message digest
+
+messageDigest :: OID
+messageDigest = [1,2,840,113549,1,9,4]
+
+-- | Return the value of the @messageDigest@ attribute.
+getMessageDigestAttr :: [Attribute] -> Maybe ByteString
+getMessageDigestAttr attrs = runParseAttribute messageDigest attrs $ do
+    OctetString d <- getNext
+    return d
+
+-- | Add or replace the @messageDigest@ attribute in a list of attributes.
+setMessageDigestAttr :: ByteString -> [Attribute] -> [Attribute]
+setMessageDigestAttr d = setAttributeASN1S messageDigest (gOctetString d)
diff --git a/src/Crypto/Store/CMS/AuthEnveloped.hs b/src/Crypto/Store/CMS/AuthEnveloped.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/AuthEnveloped.hs
@@ -0,0 +1,97 @@
+-- |
+-- Module      : Crypto.Store.CMS.AuthEnveloped
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.AuthEnveloped
+    ( AuthEnvelopedData(..)
+    , encodeAuthAttrs
+    ) where
+
+import Control.Applicative
+import Control.Monad
+
+import Data.ASN1.Types
+import Data.ByteArray (convert)
+import Data.ByteString (ByteString)
+
+import Crypto.Cipher.Types
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.Enveloped
+import Crypto.Store.CMS.OriginatorInfo
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+
+-- | Authenticated-enveloped content information.
+data AuthEnvelopedData = AuthEnvelopedData
+    { aeOriginatorInfo :: OriginatorInfo
+      -- ^ Optional information about the originator
+    , aeRecipientInfos :: [RecipientInfo]
+      -- ^ Information for recipients, allowing to decrypt the content
+    , aeContentType :: ContentType
+      -- ^ Inner content type
+    , aeContentEncryptionParams :: ASN1ObjectExact AuthContentEncryptionParams
+      -- ^ Encryption algorithm
+    , aeEncryptedContent :: EncryptedContent
+      -- ^ Encrypted content info
+    , aeAuthAttrs :: [Attribute]
+      -- ^ Optional authenticated attributes
+    , aeMAC :: MessageAuthenticationCode
+      -- ^ Message authentication code
+    , aeUnauthAttrs :: [Attribute]
+      -- ^ Optional unauthenticated attributes
+    }
+    deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P AuthEnvelopedData where
+    asn1s AuthEnvelopedData{..} =
+        asn1Container Sequence (ver . oi . ris . eci . aa . tag . ua)
+      where
+        ver = gIntVal 0
+        ris = asn1Container Set (asn1s aeRecipientInfos)
+        eci = encryptedContentInfoASN1S
+                  (aeContentType, aeContentEncryptionParams, aeEncryptedContent)
+        aa  = attributesASN1S (Container Context 1) aeAuthAttrs
+        tag = gOctetString (convert aeMAC)
+        ua  = attributesASN1S (Container Context 2) aeUnauthAttrs
+
+        oi | aeOriginatorInfo == mempty = id
+           | otherwise = originatorInfoASN1S (Container Context 0) aeOriginatorInfo
+
+instance ParseASN1Object [ASN1Event] AuthEnvelopedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v /= 0) $
+                throwParseError ("AuthEnvelopedData: parsed invalid version: " ++ show v)
+            oi <- parseOriginatorInfo (Container Context 0) <|> return mempty
+            ris <- onNextContainer Set parse
+            (ct, params, ec) <- parseEncryptedContentInfo
+            aAttrs <- parseAttributes (Container Context 1)
+            OctetString tag <- getNext
+            uAttrs <- parseAttributes (Container Context 2)
+            return AuthEnvelopedData { aeOriginatorInfo = oi
+                                     , aeContentType = ct
+                                     , aeRecipientInfos = ris
+                                     , aeContentEncryptionParams = params
+                                     , aeEncryptedContent = ec
+                                     , aeAuthAttrs = aAttrs
+                                     , aeMAC = AuthTag $ convert tag
+                                     , aeUnauthAttrs = uAttrs
+                                     }
+
+-- | Return the DER encoding of the attributes as required for AAD.
+encodeAuthAttrs :: [Attribute] -> ByteString
+encodeAuthAttrs [] = mempty
+encodeAuthAttrs l  = encodeASN1S $ asn1Container Set (asn1s l)
diff --git a/src/Crypto/Store/CMS/Encrypted.hs b/src/Crypto/Store/CMS/Encrypted.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Encrypted.hs
@@ -0,0 +1,99 @@
+-- |
+-- Module      : Crypto.Store.CMS.Encrypted
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.Encrypted
+    ( EncryptedContent
+    , ContentEncryptionKey
+    , EncryptedData(..)
+    , encryptedContentInfoASN1S
+    , parseEncryptedContentInfo
+    ) where
+
+import Control.Applicative
+import Control.Monad
+
+import           Data.ASN1.Types
+import qualified Data.ByteString as B
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+
+-- | Key used for content encryption.
+type ContentEncryptionKey = B.ByteString
+
+-- | Encrypted content.
+type EncryptedContent = B.ByteString
+
+-- | Encrypted content information.
+data EncryptedData = EncryptedData
+    { edContentType :: ContentType
+      -- ^ Inner content type
+    , edContentEncryptionParams :: ContentEncryptionParams
+      -- ^ Encryption algorithm
+    , edEncryptedContent :: EncryptedContent
+      -- ^ Encrypted content info
+    , edUnprotectedAttrs :: [Attribute]
+      -- ^ Optional unprotected attributes
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e EncryptedData where
+    asn1s EncryptedData{..} =
+        asn1Container Sequence (ver . eci . ua)
+      where
+        ver = gIntVal (if null edUnprotectedAttrs then 0 else 2)
+        eci = encryptedContentInfoASN1S
+                  (edContentType, edContentEncryptionParams, edEncryptedContent)
+        ua  = attributesASN1S (Container Context 1) edUnprotectedAttrs
+
+instance Monoid e => ParseASN1Object e EncryptedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v /= 0 && v /= 2) $
+                throwParseError ("EncryptedData: parsed invalid version: " ++ show v)
+            (ct, params, ec) <- parseEncryptedContentInfo
+            attrs <- parseAttributes (Container Context 1)
+            return EncryptedData { edContentType = ct
+                                 , edContentEncryptionParams = params
+                                 , edEncryptedContent = ec
+                                 , edUnprotectedAttrs = attrs
+                                 }
+
+-- | Generate ASN.1 for EncryptedContentInfo.
+encryptedContentInfoASN1S :: (ASN1Elem e, ProduceASN1Object e alg)
+                          => (ContentType, alg, B.ByteString) -> ASN1Stream e
+encryptedContentInfoASN1S (ct, alg, ec) =
+    asn1Container Sequence (ct' . alg' . ec')
+  where
+    ct'  = gOID (getObjectID ct)
+    alg' = asn1s alg
+    ec'  = asn1Container (Container Context 0) (gOctetString ec)
+
+-- | Parse EncryptedContentInfo from ASN.1.
+parseEncryptedContentInfo :: ParseASN1Object e alg
+                          => ParseASN1 e (ContentType, alg, B.ByteString)
+parseEncryptedContentInfo = onNextContainer Sequence $ do
+    OID oid <- getNext
+    alg <- parse
+    ec <- parseEncryptedContent
+    withObjectID "content type" oid $ \ct -> return (ct, alg, ec)
+  where
+    parseEncryptedContent = parseWrapped <|> parsePrimitive
+    parseWrapped  = onNextContainer (Container Context 0) parseOctetStrings
+    parsePrimitive = do Other Context 0 bs <- getNext; return bs
+    parseOctetString = do OctetString bs <- getNext; return bs
+    parseOctetStrings = B.concat <$> getMany parseOctetString
diff --git a/src/Crypto/Store/CMS/Enveloped.hs b/src/Crypto/Store/CMS/Enveloped.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Enveloped.hs
@@ -0,0 +1,631 @@
+-- |
+-- Module      : Crypto.Store.CMS.Enveloped
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.Enveloped
+    ( EncryptedKey
+    , UserKeyingMaterial
+    , RecipientInfo(..)
+    , EnvelopedData(..)
+    , ProducerOfRI
+    , ConsumerOfRI
+    -- * Key Transport recipients
+    , KTRecipientInfo(..)
+    , RecipientIdentifier(..)
+    , IssuerAndSerialNumber(..)
+    , forKeyTransRecipient
+    , withRecipientKeyTrans
+    -- * Key Agreement recipients
+    , KARecipientInfo(..)
+    , OriginatorIdentifierOrKey(..)
+    , OriginatorPublicKey
+    , RecipientEncryptedKey(..)
+    , KeyAgreeRecipientIdentifier(..)
+    , forKeyAgreeRecipient
+    , withRecipientKeyAgree
+    -- * Key Encryption Key recipients
+    , KeyEncryptionKey
+    , KEKRecipientInfo(..)
+    , KeyIdentifier(..)
+    , OtherKeyAttribute(..)
+    , forKeyRecipient
+    , withRecipientKey
+    -- * Password recipients
+    , Password
+    , PasswordRecipientInfo(..)
+    , forPasswordRecipient
+    , withRecipientPassword
+    ) where
+
+import Control.Applicative
+import Control.Monad
+
+import Data.ASN1.BitArray
+import Data.ASN1.Types
+import Data.ByteString (ByteString)
+import Data.List (find)
+import Data.Maybe (fromMaybe)
+import Data.X509
+
+import Time.Types
+
+import Crypto.Random (MonadRandom)
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.OriginatorInfo
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+import Crypto.Store.PKCS8.EC
+
+-- | Encrypted key.
+type EncryptedKey = ByteString
+
+-- | User keying material.
+type UserKeyingMaterial = ByteString
+
+-- | Key used for key encryption.
+type KeyEncryptionKey = ByteString
+
+-- | A password stored as a sequence of UTF-8 bytes.
+--
+-- Some key-derivation functions add restrictions to what characters
+-- are supported.
+type Password = ByteString
+
+-- | Union type related to identification of the recipient.
+data RecipientIdentifier
+    = RecipientIASN IssuerAndSerialNumber  -- ^ Issuer and Serial Number
+    | RecipientSKI  ByteString             -- ^ Subject Key Identifier
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e RecipientIdentifier where
+    asn1s (RecipientIASN iasn) = asn1s iasn
+    asn1s (RecipientSKI  ski)  = asn1Container (Container Context 0)
+                                    (gOctetString ski)
+
+instance Monoid e => ParseASN1Object e RecipientIdentifier where
+    parse = parseIASN <|> parseSKI
+      where parseIASN = RecipientIASN <$> parse
+            parseSKI  = RecipientSKI  <$>
+                onNextContainer (Container Context 0) parseBS
+            parseBS = do { OctetString bs <- getNext; return bs }
+
+getKTVersion :: RecipientIdentifier -> Integer
+getKTVersion (RecipientIASN _) = 0
+getKTVersion (RecipientSKI _)  = 2
+
+-- | Identification of a certificate using the issuer DN and serial number.
+data IssuerAndSerialNumber = IssuerAndSerialNumber
+    { iasnIssuer :: DistinguishedName
+      -- ^ Distinguished name of the certificate issuer
+    , iasnSerial :: Integer
+      -- ^ Issuer-specific certificate serial number
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e IssuerAndSerialNumber where
+    asn1s IssuerAndSerialNumber{..} =
+        asn1Container Sequence (asn1s iasnIssuer . gIntVal iasnSerial)
+
+instance Monoid e => ParseASN1Object e IssuerAndSerialNumber where
+    parse = onNextContainer Sequence $ do
+        i <- parse
+        IntVal s <- getNext
+        return IssuerAndSerialNumber { iasnIssuer = i
+                                     , iasnSerial = s
+                                     }
+
+idEcPublicKey :: OID
+idEcPublicKey = [1,2,840,10045,2,1]
+
+-- | Originator public key used for key-agreement.  Contrary to 'PubKey' the
+-- domain parameters are not used and may be left empty.
+data OriginatorPublicKey = OriginatorPublicKeyEC [ASN1] BitArray
+    deriving (Show,Eq)
+
+originatorPublicKeyASN1S :: ASN1Elem e
+                         => ASN1ConstructionType
+                         -> OriginatorPublicKey
+                         -> ASN1Stream e
+originatorPublicKeyASN1S ty (OriginatorPublicKeyEC asn1 ba) =
+    asn1Container ty (alg . gBitString ba)
+  where
+    alg = asn1Container Sequence (gOID idEcPublicKey . gMany asn1)
+
+parseOriginatorPublicKey :: Monoid e
+                         => ASN1ConstructionType
+                         -> ParseASN1 e OriginatorPublicKey
+parseOriginatorPublicKey ty =
+    onNextContainer ty $ do
+        asn1 <- onNextContainer Sequence $ do
+                    OID oid <- getNext
+                    guard (oid == idEcPublicKey)
+                    getMany getNext
+        BitString ba <- getNext
+        return (OriginatorPublicKeyEC asn1 ba)
+
+-- | Union type related to identification of the originator.
+data OriginatorIdentifierOrKey
+    = OriginatorIASN IssuerAndSerialNumber  -- ^ Issuer and Serial Number
+    | OriginatorSKI  ByteString             -- ^ Subject Key Identifier
+    | OriginatorPublic OriginatorPublicKey  -- ^ Anonymous public key
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e OriginatorIdentifierOrKey where
+    asn1s (OriginatorIASN iasn)   = asn1s iasn
+    asn1s (OriginatorSKI  ski)    = asn1Container (Container Context 0)
+                                       (gOctetString ski)
+    asn1s (OriginatorPublic pub)  =
+        originatorPublicKeyASN1S (Container Context 1) pub
+
+instance Monoid e => ParseASN1Object e OriginatorIdentifierOrKey where
+    parse = parseIASN <|> parseSKI <|> parsePublic
+      where parseIASN = OriginatorIASN <$> parse
+            parseSKI  = OriginatorSKI  <$>
+                onNextContainer (Container Context 0) parseBS
+            parseBS = do { OctetString bs <- getNext; return bs }
+            parsePublic  = OriginatorPublic <$>
+                parseOriginatorPublicKey (Container Context 1)
+
+-- | Union type related to identification of a key-agreement recipient.
+data KeyAgreeRecipientIdentifier
+    = KeyAgreeRecipientIASN IssuerAndSerialNumber  -- ^ Issuer and Serial Number
+    | KeyAgreeRecipientKI   KeyIdentifier          -- ^ Key identifier
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e KeyAgreeRecipientIdentifier where
+    asn1s (KeyAgreeRecipientIASN iasn) = asn1s iasn
+    asn1s (KeyAgreeRecipientKI   ki)   = asn1Container (Container Context 0)
+                                            (asn1s ki)
+
+instance Monoid e => ParseASN1Object e KeyAgreeRecipientIdentifier where
+    parse = parseIASN <|> parseKI
+      where parseIASN = KeyAgreeRecipientIASN <$> parse
+            parseKI   = KeyAgreeRecipientKI   <$>
+                onNextContainer (Container Context 0) parse
+
+-- | Encrypted key for a recipient in a key-agreement RI.
+data RecipientEncryptedKey = RecipientEncryptedKey
+    { rekRid :: KeyAgreeRecipientIdentifier -- ^ identifier of recipient
+    , rekEncryptedKey :: EncryptedKey       -- ^ encrypted content-encryption key
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e RecipientEncryptedKey where
+    asn1s RecipientEncryptedKey{..} = asn1Container Sequence (rid . ek)
+      where rid = asn1s rekRid
+            ek  = gOctetString rekEncryptedKey
+
+instance Monoid e => ParseASN1Object e RecipientEncryptedKey where
+    parse = onNextContainer Sequence $ do
+        rid <- parse
+        OctetString ek <- getNext
+        return RecipientEncryptedKey { rekRid = rid, rekEncryptedKey = ek }
+
+findRecipientEncryptedKey :: SignedCertificate
+                          -> [RecipientEncryptedKey]
+                          -> Maybe EncryptedKey
+findRecipientEncryptedKey cert list = rekEncryptedKey <$> find fn list
+  where
+    c = signedObject (getSigned cert)
+    matchIASN iasn =
+        (iasnIssuer iasn, iasnSerial iasn) == (certIssuerDN c, certSerial c)
+    matchSKI ski   =
+        case extensionGet (certExtensions c) of
+            Just (ExtSubjectKeyId idBs) -> idBs == ski
+            Nothing                     -> False
+    fn rek = case rekRid rek of
+                 KeyAgreeRecipientIASN iasn -> matchIASN iasn
+                 KeyAgreeRecipientKI   ki   -> matchSKI (keyIdentifier ki)
+
+-- | Additional information in a 'KeyIdentifier'.
+data OtherKeyAttribute = OtherKeyAttribute
+    { keyAttrId :: OID    -- ^ attribute identifier
+    , keyAttr   :: [ASN1] -- ^ attribute value
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e OtherKeyAttribute where
+    asn1s OtherKeyAttribute{..} = asn1Container Sequence (attrId . attr)
+      where attrId = gOID keyAttrId
+            attr   = gMany keyAttr
+
+instance Monoid e => ParseASN1Object e OtherKeyAttribute where
+    parse = onNextContainer Sequence $ do
+        OID attrId <- getNext
+        attr <- getMany getNext
+        return OtherKeyAttribute { keyAttrId = attrId, keyAttr = attr }
+
+-- | Key identifier and optional attributes.
+data KeyIdentifier = KeyIdentifier
+    { keyIdentifier :: ByteString         -- ^ identifier of the key
+    , keyDate :: Maybe DateTime           -- ^ optional timestamp
+    , keyOther :: Maybe OtherKeyAttribute -- ^ optional information
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e KeyIdentifier where
+    asn1s KeyIdentifier{..} = asn1Container Sequence (keyId . date . other)
+      where
+        keyId = gOctetString keyIdentifier
+        date  = optASN1S keyDate $ \v -> gASN1Time TimeGeneralized v Nothing
+        other = optASN1S keyOther asn1s
+
+instance Monoid e => ParseASN1Object e KeyIdentifier where
+    parse = onNextContainer Sequence $ do
+        OctetString keyId <- getNext
+        date <- getNextMaybe dateTimeOrNothing
+        b <- hasNext
+        other <- if b then Just <$> parse else return Nothing
+        return KeyIdentifier { keyIdentifier = keyId
+                             , keyDate = date
+                             , keyOther = other
+                             }
+
+-- | Recipient using key transport.
+data KTRecipientInfo = KTRecipientInfo
+    { ktRid :: RecipientIdentifier                 -- ^ identifier of recipient
+    , ktKeyTransportParams :: KeyTransportParams   -- ^ key transport algorithm
+    , ktEncryptedKey :: EncryptedKey               -- ^ encrypted content-encryption key
+    }
+    deriving (Show,Eq)
+
+-- | Recipient using key agreement.
+data KARecipientInfo = KARecipientInfo
+    { kaOriginator :: OriginatorIdentifierOrKey           -- ^ identifier of orginator or anonymous key
+    , kaUkm        :: Maybe UserKeyingMaterial            -- ^ user keying material
+    , kaKeyAgreementParams :: KeyAgreementParams          -- ^ key agreement algorithm
+    , kaRecipientEncryptedKeys :: [RecipientEncryptedKey] -- ^ encrypted content-encryption key for one or multiple recipients
+    }
+    deriving (Show,Eq)
+
+-- | Recipient using key encryption.
+data KEKRecipientInfo = KEKRecipientInfo
+    { kekId :: KeyIdentifier                        -- ^ identifier of key encryption key
+    , kekKeyEncryptionParams :: KeyEncryptionParams -- ^ key encryption algorithm
+    , kekEncryptedKey :: EncryptedKey               -- ^ encrypted content-encryption key
+    }
+    deriving (Show,Eq)
+
+-- | Recipient using password-based protection.
+data PasswordRecipientInfo = PasswordRecipientInfo
+    { priKeyDerivationFunc :: KeyDerivationFunc     -- ^ function to derive key
+    , priKeyEncryptionParams :: KeyEncryptionParams -- ^ key encryption algorithm
+    , priEncryptedKey :: EncryptedKey               -- ^ encrypted content-encryption key
+    }
+    deriving (Show,Eq)
+
+-- | Information for a recipient of an 'EnvelopedData'.  An element contains
+-- the content-encryption key in encrypted form.
+data RecipientInfo = KTRI KTRecipientInfo
+                     -- ^ Recipient using key transport
+                   | KARI KARecipientInfo
+                     -- ^ Recipient using key agreement
+                   | KEKRI KEKRecipientInfo
+                     -- ^ Recipient using key encryption
+                   | PasswordRI PasswordRecipientInfo
+                     -- ^ Recipient using password-based protection
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e RecipientInfo where
+    asn1s (KTRI KTRecipientInfo{..}) =
+        asn1Container Sequence (ver . rid . ktp . ek)
+      where
+        ver = gIntVal (getKTVersion ktRid)
+        rid = asn1s ktRid
+        ktp = algorithmASN1S Sequence ktKeyTransportParams
+        ek  = gOctetString ktEncryptedKey
+
+    asn1s (KARI KARecipientInfo{..}) =
+        asn1Container (Container Context 1) (ver . ori . ukm . kap . reks)
+      where
+        ver  = gIntVal 3
+        ori  = asn1Container (Container Context 0) (asn1s kaOriginator)
+        kap  = algorithmASN1S Sequence kaKeyAgreementParams
+        reks = asn1Container Sequence (asn1s kaRecipientEncryptedKeys)
+
+        ukm = case kaUkm of
+                  Nothing -> id
+                  Just bs -> asn1Container (Container Context 1) (gOctetString bs)
+
+    asn1s (KEKRI KEKRecipientInfo{..}) =
+        asn1Container (Container Context 2) (ver . kid . kep . ek)
+      where
+        ver = gIntVal 4
+        kid = asn1s kekId
+        kep = algorithmASN1S Sequence kekKeyEncryptionParams
+        ek  = gOctetString kekEncryptedKey
+
+    asn1s (PasswordRI PasswordRecipientInfo{..}) =
+        asn1Container (Container Context 3) (ver . kdf . kep . ek)
+      where
+        ver = gIntVal 0
+        kdf = algorithmASN1S (Container Context 0) priKeyDerivationFunc
+        kep = algorithmASN1S Sequence priKeyEncryptionParams
+        ek  = gOctetString priEncryptedKey
+
+instance Monoid e => ParseASN1Object e RecipientInfo where
+    parse = do
+        c <- onNextContainerMaybe Sequence parseKT
+             `orElse` onNextContainerMaybe (Container Context 1) parseKA
+             `orElse` onNextContainerMaybe (Container Context 2) parseKEK
+             `orElse` onNextContainerMaybe (Container Context 3) parsePassword
+        case c of
+            Just val -> return val
+            Nothing  -> throwParseError "RecipientInfo: unable to parse"
+      where
+        parseKT = KTRI <$> do
+            IntVal v <- getNext
+            when (v `notElem` [0, 2]) $
+                throwParseError ("RecipientInfo: parsed invalid KT version: " ++ show v)
+            rid <- parse
+            ktp <- parseAlgorithm Sequence
+            (OctetString ek) <- getNext
+            return KTRecipientInfo { ktRid = rid
+                                   , ktKeyTransportParams = ktp
+                                   , ktEncryptedKey = ek
+                                   }
+
+        parseKA = KARI <$> do
+            IntVal 3 <- getNext
+            ori <- onNextContainer (Container Context 0) parse
+            ukm <- onNextContainerMaybe (Container Context 1) $
+                       do { OctetString bs <- getNext; return bs }
+            kap <- parseAlgorithm Sequence
+            reks <- onNextContainer Sequence parse
+            return KARecipientInfo { kaOriginator = ori
+                                   , kaUkm = ukm
+                                   , kaKeyAgreementParams = kap
+                                   , kaRecipientEncryptedKeys = reks
+                                   }
+
+        parseKEK = KEKRI <$> do
+            IntVal 4 <- getNext
+            kid <- parse
+            kep <- parseAlgorithm Sequence
+            (OctetString ek) <- getNext
+            return KEKRecipientInfo { kekId = kid
+                                    , kekKeyEncryptionParams = kep
+                                    , kekEncryptedKey = ek
+                                    }
+
+        parsePassword = PasswordRI <$> do
+            IntVal 0 <- getNext
+            kdf <- parseAlgorithm (Container Context 0)
+            kep <- parseAlgorithm Sequence
+            (OctetString ek) <- getNext
+            return PasswordRecipientInfo { priKeyDerivationFunc = kdf
+                                         , priKeyEncryptionParams = kep
+                                         , priEncryptedKey = ek
+                                         }
+
+isVersion0 :: RecipientInfo -> Bool
+isVersion0 (KTRI x)       = getKTVersion (ktRid x) == 0
+isVersion0 (KARI _)       = False      -- because version is always 3
+isVersion0 (KEKRI _)      = False      -- because version is always 4
+isVersion0 (PasswordRI _) = True       -- because version is always 0
+
+isPwriOri :: RecipientInfo -> Bool
+isPwriOri (KTRI _)       = False
+isPwriOri (KARI _)       = False
+isPwriOri (KEKRI _)      = False
+isPwriOri (PasswordRI _) = True
+
+-- | Enveloped content information.
+data EnvelopedData = EnvelopedData
+    { evOriginatorInfo :: OriginatorInfo
+      -- ^ Optional information about the originator
+    , evRecipientInfos :: [RecipientInfo]
+      -- ^ Information for recipients, allowing to decrypt the content
+    , evContentType :: ContentType
+      -- ^ Inner content type
+    , evContentEncryptionParams :: ContentEncryptionParams
+      -- ^ Encryption algorithm
+    , evEncryptedContent :: EncryptedContent
+      -- ^ Encrypted content info
+    , evUnprotectedAttrs :: [Attribute]
+      -- ^ Optional unprotected attributes
+    }
+    deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P EnvelopedData where
+    asn1s EnvelopedData{..} =
+        asn1Container Sequence (ver . oi . ris . eci . ua)
+      where
+        ver = gIntVal v
+        ris = asn1Container Set (asn1s evRecipientInfos)
+        eci = encryptedContentInfoASN1S
+                  (evContentType, evContentEncryptionParams, evEncryptedContent)
+        ua  = attributesASN1S (Container Context 1) evUnprotectedAttrs
+
+        oi | evOriginatorInfo == mempty = id
+           | otherwise = originatorInfoASN1S (Container Context 0) evOriginatorInfo
+
+        v | hasChoiceOther evOriginatorInfo = 4
+          | any isPwriOri evRecipientInfos  = 3
+          | evOriginatorInfo /= mempty      = 2
+          | not (null evUnprotectedAttrs)   = 2
+          | all isVersion0 evRecipientInfos = 0
+          | otherwise                       = 2
+
+instance ParseASN1Object [ASN1Event] EnvelopedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v > 4) $
+                throwParseError ("EnvelopedData: parsed invalid version: " ++ show v)
+            oi <- parseOriginatorInfo (Container Context 0) <|> return mempty
+            ris <- onNextContainer Set parse
+            (ct, params, ec) <- parseEncryptedContentInfo
+            attrs <- parseAttributes (Container Context 1)
+            return EnvelopedData { evOriginatorInfo = oi
+                                 , evRecipientInfos = ris
+                                 , evContentType = ct
+                                 , evContentEncryptionParams = params
+                                 , evEncryptedContent = ec
+                                 , evUnprotectedAttrs = attrs
+                                 }
+
+-- | Function able to produce a 'RecipientInfo'.
+type ProducerOfRI m = ContentEncryptionKey -> m (Either StoreError RecipientInfo)
+
+-- | Function able to consume a 'RecipientInfo'.
+type ConsumerOfRI m = RecipientInfo -> m (Either StoreError ContentEncryptionKey)
+
+-- | Generate a Key Transport recipient from a certificate and
+-- desired algorithm.  The recipient will contain certificate identifier.
+--
+-- This function can be used as parameter to 'Crypto.Store.CMS.envelopData'.
+forKeyTransRecipient :: MonadRandom m
+                     => SignedCertificate -> KeyTransportParams -> ProducerOfRI m
+forKeyTransRecipient cert params inkey = do
+    ek <- transportEncrypt params (certPubKey obj) inkey
+    return (KTRI . build <$> ek)
+  where
+    obj = signedObject (getSigned cert)
+    isn = IssuerAndSerialNumber (certIssuerDN obj) (certSerial obj)
+
+    build ek = KTRecipientInfo
+                  { ktRid = RecipientIASN isn
+                  , ktKeyTransportParams = params
+                  , ktEncryptedKey = ek
+                  }
+
+-- | Use a Key Transport recipient, knowing the private key.
+--
+-- This function can be used as parameter to
+-- 'Crypto.Store.CMS.openEnvelopedData'.
+withRecipientKeyTrans :: MonadRandom m => PrivKey -> ConsumerOfRI m
+withRecipientKeyTrans privKey (KTRI KTRecipientInfo{..}) =
+    transportDecrypt ktKeyTransportParams privKey ktEncryptedKey
+withRecipientKeyTrans _ _ = pure (Left RecipientTypeMismatch)
+
+-- | Generate a Key Agreement recipient from a certificate and
+-- desired algorithm.  The recipient info will contain an ephemeral public key.
+--
+-- This function can be used as parameter to 'Crypto.Store.CMS.envelopData'.
+--
+-- To avoid decreasing the security strength, Key Encryption parameters should
+-- use a key size equal or greater than the content encryption key.
+forKeyAgreeRecipient :: MonadRandom m
+                     => SignedCertificate -> KeyAgreementParams -> ProducerOfRI m
+forKeyAgreeRecipient cert params inkey = do
+    ephemeral <- ecdhGenerate (certPubKey obj)
+    case ephemeral of
+        Right (curve, d, sp) -> do
+            let SerializedPoint pt = getSerializedPoint curve d
+                aPub = OriginatorPublicKeyEC [] (toBitArray pt 0)
+            ek <- ecdhEncrypt params Nothing curve d sp inkey
+            return (KARI . build aPub <$> ek)
+        Left err -> return $ Left err
+  where
+    obj = signedObject (getSigned cert)
+    isn = IssuerAndSerialNumber (certIssuerDN obj) (certSerial obj)
+
+    makeREK ek = RecipientEncryptedKey
+                     { rekRid = KeyAgreeRecipientIASN isn
+                     , rekEncryptedKey = ek
+                     }
+
+    build aPub ek =
+        KARecipientInfo
+            { kaOriginator = OriginatorPublic aPub
+            , kaUkm = Nothing
+            , kaKeyAgreementParams = params
+            , kaRecipientEncryptedKeys = [ makeREK ek ]
+            }
+
+-- | Use a Key Agreement recipient, knowing the recipient private key.  The
+-- recipient certificate is also required to locate which encrypted key to use.
+--
+-- This function can be used as parameter to
+-- 'Crypto.Store.CMS.openEnvelopedData'.
+withRecipientKeyAgree :: MonadRandom m => PrivKey -> SignedCertificate -> ConsumerOfRI m
+withRecipientKeyAgree (PrivKeyEC priv) cert (KARI KARecipientInfo{..}) =
+    case kaOriginator of
+        OriginatorPublic (OriginatorPublicKeyEC _ ba) ->
+            case findRecipientEncryptedKey cert kaRecipientEncryptedKeys of
+                Nothing -> pure (Left RecipientKeyNotFound)
+                Just ek ->
+                    let pub = SerializedPoint (bitArrayGetData ba)
+                     in pure (ecdhDecrypt kaKeyAgreementParams kaUkm priv pub ek)
+        _ -> pure (Left UnsupportedOriginatorFormat)
+withRecipientKeyAgree _ _ (KARI _) = pure (Left UnexpectedPrivateKeyType)
+withRecipientKeyAgree _ _ _        = pure (Left RecipientTypeMismatch)
+
+-- | Generate a Key Encryption Key recipient from a key encryption key and
+-- desired algorithm.  The recipient may identify the KEK that was used with
+-- the supplied identifier.
+--
+-- This function can be used as parameter to 'Crypto.Store.CMS.envelopData'.
+--
+-- To avoid decreasing the security strength, Key Encryption parameters should
+-- use a key size equal or greater than the content encryption key.
+forKeyRecipient :: MonadRandom m
+                => KeyEncryptionKey
+                -> KeyIdentifier
+                -> KeyEncryptionParams
+                -> ProducerOfRI m
+forKeyRecipient key kid params inkey = do
+    ek <- keyEncrypt key params inkey
+    return (KEKRI . build <$> ek)
+  where
+    build ek = KEKRecipientInfo
+                   { kekId = kid
+                   , kekKeyEncryptionParams = params
+                   , kekEncryptedKey = ek
+                   }
+
+-- | Use a Key Encryption Key recipient, knowing the key encryption key.
+--
+-- This function can be used as parameter to
+-- 'Crypto.Store.CMS.openEnvelopedData'.
+withRecipientKey :: Applicative f => KeyEncryptionKey -> ConsumerOfRI f
+withRecipientKey key (KEKRI KEKRecipientInfo{..}) =
+    pure (keyDecrypt key kekKeyEncryptionParams kekEncryptedKey)
+withRecipientKey _ _ = pure (Left RecipientTypeMismatch)
+
+-- | Generate a password recipient from a password.
+--
+-- This function can be used as parameter to 'Crypto.Store.CMS.envelopData'.
+forPasswordRecipient :: MonadRandom m
+                     => Password
+                     -> KeyDerivationFunc
+                     -> KeyEncryptionParams
+                     -> ProducerOfRI m
+forPasswordRecipient pwd kdf params inkey = do
+    ek <- keyEncrypt derived params inkey
+    return (PasswordRI . build <$> ek)
+  where
+    derived = kdfDerive kdf len pwd :: EncryptedKey
+    len = fromMaybe (getMaximumKeySize params) (kdfKeyLength kdf)
+    build ek = PasswordRecipientInfo
+                   { priKeyDerivationFunc = kdf
+                   , priKeyEncryptionParams = params
+                   , priEncryptedKey = ek
+                   }
+
+-- | Use a password recipient, knowing the password.
+--
+-- This function can be used as parameter to
+-- 'Crypto.Store.CMS.openEnvelopedData'.
+withRecipientPassword :: Applicative f => Password -> ConsumerOfRI f
+withRecipientPassword pwd (PasswordRI PasswordRecipientInfo{..}) =
+    pure (keyDecrypt derived priKeyEncryptionParams priEncryptedKey)
+  where
+    derived = kdfDerive priKeyDerivationFunc len pwd :: EncryptedKey
+    len = fromMaybe (getMaximumKeySize priKeyEncryptionParams)
+                    (kdfKeyLength priKeyDerivationFunc)
+withRecipientPassword _ _ = pure (Left RecipientTypeMismatch)
diff --git a/src/Crypto/Store/CMS/Info.hs b/src/Crypto/Store/CMS/Info.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Info.hs
@@ -0,0 +1,353 @@
+-- |
+-- Module      : Crypto.Store.CMS.Info
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- CMS content information.
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeFamilies #-}
+module Crypto.Store.CMS.Info
+    ( ContentInfo(..)
+    , getContentType
+    , SignedData(..)
+    , DigestedData(..)
+    , AuthenticatedData(..)
+    , encapsulate
+    , decapsulate
+    ) where
+
+import Control.Applicative
+import Control.Monad
+
+import           Data.ASN1.BinaryEncoding
+import           Data.ASN1.Encoding
+import           Data.ASN1.Types
+import           Data.ByteString (ByteString)
+import qualified Data.ByteArray as B
+import           Data.Maybe (fromMaybe)
+
+import Crypto.Cipher.Types
+import Crypto.Hash hiding (MD5)
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.AuthEnveloped
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.Enveloped
+import Crypto.Store.CMS.OriginatorInfo
+import Crypto.Store.CMS.Signed
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+import Crypto.Store.Util
+
+-- | Get the type of a content info.
+getContentType :: ContentInfo -> ContentType
+getContentType (DataCI _)              = DataType
+getContentType (SignedDataCI _)        = SignedDataType
+getContentType (EnvelopedDataCI _)     = EnvelopedDataType
+getContentType (DigestedDataCI _)      = DigestedDataType
+getContentType (EncryptedDataCI _)     = EncryptedDataType
+getContentType (AuthenticatedDataCI _) = AuthenticatedDataType
+getContentType (AuthEnvelopedDataCI _) = AuthEnvelopedDataType
+
+
+-- ContentInfo
+
+-- | CMS content information.
+data ContentInfo = DataCI ByteString                     -- ^ Arbitrary octet string
+                 | SignedDataCI SignedData               -- ^ Signed content info
+                 | EnvelopedDataCI EnvelopedData         -- ^ Enveloped content info
+                 | DigestedDataCI DigestedData           -- ^ Content info with associated digest
+                 | EncryptedDataCI EncryptedData         -- ^ Encrypted content info
+                 | AuthenticatedDataCI AuthenticatedData -- ^ Authenticatedcontent info
+                 | AuthEnvelopedDataCI AuthEnvelopedData -- ^ Authenticated-enveloped content info
+                 deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P ContentInfo where
+    asn1s ci = asn1Container Sequence (oid . cont)
+      where oid = gOID $ getObjectID $ getContentType ci
+            cont = asn1Container (Container Context 0) inner
+            inner =
+                case ci of
+                    DataCI bs              -> dataASN1S bs
+                    SignedDataCI ed        -> asn1s ed
+                    EnvelopedDataCI ed     -> asn1s ed
+                    DigestedDataCI dd      -> asn1s dd
+                    EncryptedDataCI ed     -> asn1s ed
+                    AuthenticatedDataCI ad -> asn1s ad
+                    AuthEnvelopedDataCI ae -> asn1s ae
+
+instance ParseASN1Object [ASN1Event] ContentInfo where
+    parse =
+        onNextContainer Sequence $ do
+            OID oid <- getNext
+            withObjectID "content type" oid $ \ct ->
+                onNextContainer (Container Context 0) (parseInner ct)
+      where
+        parseInner DataType              = DataCI <$> parseData
+        parseInner SignedDataType        = SignedDataCI <$> parse
+        parseInner EnvelopedDataType     = EnvelopedDataCI <$> parse
+        parseInner DigestedDataType      = DigestedDataCI <$> parse
+        parseInner EncryptedDataType     = EncryptedDataCI <$> parse
+        parseInner AuthenticatedDataType = AuthenticatedDataCI <$> parse
+        parseInner AuthEnvelopedDataType = AuthEnvelopedDataCI <$> parse
+
+
+-- Data
+
+dataASN1S :: ASN1Elem e => ByteString -> ASN1Stream e
+dataASN1S = gOctetString
+
+parseData :: Monoid e => ParseASN1 e ByteString
+parseData = do
+    next <- getNext
+    case next of
+        OctetString bs -> return bs
+        _              -> throwParseError "Data: parsed unexpected content"
+
+isData :: ContentInfo -> Bool
+isData (DataCI _) = True
+isData _          = False
+
+
+-- SignedData
+
+-- | Signed content information.
+data SignedData = SignedData
+    { sdDigestAlgorithms :: [DigestAlgorithm] -- ^ Digest algorithms
+    , sdContentInfo :: ContentInfo            -- ^ Inner content info
+    , sdCertificates :: [CertificateChoice]   -- ^ The collection of certificates
+    , sdCRLs  :: [RevocationInfoChoice]       -- ^ The collection of CRLs
+    , sdSignerInfos :: [SignerInfo]           -- ^ Per-signer information
+    }
+    deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P SignedData where
+    asn1s SignedData{..} =
+        asn1Container Sequence (ver . dig . ci . certs . crls . sis)
+      where
+        ver = gIntVal v
+        dig = asn1Container Set (digestTypesASN1S sdDigestAlgorithms)
+        ci  = encapsulatedContentInfoASN1S sdContentInfo
+        certs = gen 0 sdCertificates
+        crls  = gen 1 sdCRLs
+        sis = asn1Container Set (asn1s sdSignerInfos)
+
+        gen tag list
+            | null list = id
+            | otherwise = asn1Container (Container Context tag) (asn1s list)
+
+        v | hasChoiceOther sdCertificates = 5
+          | hasChoiceOther sdCRLs         = 5
+          | any isVersion3 sdSignerInfos  = 3
+          | isData sdContentInfo          = 1
+          | otherwise                     = 3
+
+
+instance ParseASN1Object [ASN1Event] SignedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v > 5) $
+                throwParseError ("SignedData: parsed invalid version: " ++ show v)
+            dig <- onNextContainer Set parseDigestTypes
+            inner <- parseEncapsulatedContentInfo
+            certs <- parseOptList 0
+            crls  <- parseOptList 1
+            sis <- onNextContainer Set parse
+            return SignedData { sdDigestAlgorithms = dig
+                              , sdContentInfo = inner
+                              , sdCertificates = certs
+                              , sdCRLs = crls
+                              , sdSignerInfos = sis
+                              }
+      where
+        parseOptList tag =
+            fromMaybe [] <$> onNextContainerMaybe (Container Context tag) parse
+
+digestTypesASN1S :: ASN1Elem e => [DigestAlgorithm] -> ASN1Stream e
+digestTypesASN1S list cont = foldr (algorithmASN1S Sequence) cont list
+
+parseDigestTypes :: Monoid e => ParseASN1 e [DigestAlgorithm]
+parseDigestTypes = getMany (parseAlgorithm Sequence)
+
+
+-- DigestedData
+
+-- | Digested content information.
+data DigestedData = forall hashAlg. HashAlgorithm hashAlg => DigestedData
+    { ddDigestAlgorithm :: DigestProxy hashAlg     -- ^ Digest algorithm
+    , ddContentInfo :: ContentInfo                 -- ^ Inner content info
+    , ddDigest :: Digest hashAlg                   -- ^ Digest value
+    }
+
+instance Show DigestedData where
+    showsPrec d DigestedData{..} = showParen (d > 10) $
+        showString "DigestedData "
+            . showString "{ ddDigestAlgorithm = " . shows ddDigestAlgorithm
+            . showString ", ddContentInfo = " . shows ddContentInfo
+            . showString ", ddDigest = " . shows ddDigest
+            . showString " }"
+
+instance Eq DigestedData where
+    DigestedData a1 i1 d1 == DigestedData a2 i2 d2 =
+        DigestAlgorithm a1 == DigestAlgorithm a2 && d1 `B.eq` d2 && i1 == i2
+
+instance ASN1Elem e => ProduceASN1Object e DigestedData where
+    asn1s DigestedData{..} =
+        asn1Container Sequence (ver . alg . ci . dig)
+      where
+        v = if isData ddContentInfo then 0 else 2
+        d = DigestAlgorithm ddDigestAlgorithm
+
+        ver = gIntVal v
+        alg = algorithmASN1S Sequence d
+        ci  = encapsulatedContentInfoASN1S ddContentInfo
+        dig = gOctetString (B.convert ddDigest)
+
+instance Monoid e => ParseASN1Object e DigestedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v /= 0 && v /= 2) $
+                throwParseError ("DigestedData: parsed invalid version: " ++ show v)
+            alg <- parseAlgorithm Sequence
+            inner <- parseEncapsulatedContentInfo
+            OctetString bs <- getNext
+            case alg of
+                DigestAlgorithm digAlg ->
+                    case digestFromByteString bs of
+                        Nothing -> throwParseError "DigestedData: parsed invalid digest"
+                        Just d  ->
+                            return DigestedData { ddDigestAlgorithm = digAlg
+                                                , ddContentInfo = inner
+                                                , ddDigest = d
+                                                }
+
+
+-- Authenticated data
+
+-- | Authenticated content information.
+data AuthenticatedData = AuthenticatedData
+    { adOriginatorInfo :: OriginatorInfo
+      -- ^ Optional information about the originator
+    , adRecipientInfos :: [RecipientInfo]
+      -- ^ Information for recipients, allowing to authenticate the content
+    , adMACAlgorithm :: MACAlgorithm
+      -- ^ MAC algorithm
+    , adDigestAlgorithm :: Maybe DigestAlgorithm
+      -- ^ Optional digest algorithm
+    , adContentInfo :: ContentInfo
+      -- ^ Inner content info
+    , adAuthAttrs :: [Attribute]
+      -- ^ Optional authenticated attributes
+    , adMAC :: MessageAuthenticationCode
+      -- ^ Message authentication code
+    , adUnauthAttrs :: [Attribute]
+      -- ^ Optional unauthenticated attributes
+    }
+    deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P AuthenticatedData where
+    asn1s AuthenticatedData{..} =
+        asn1Container Sequence (ver . oi . ris . alg . dig . ci . aa . tag . ua)
+      where
+        ver = gIntVal v
+        ris = asn1Container Set (asn1s adRecipientInfos)
+        alg = algorithmASN1S Sequence adMACAlgorithm
+        dig = algorithmMaybeASN1S (Container Context 1) adDigestAlgorithm
+        ci  = encapsulatedContentInfoASN1S adContentInfo
+        aa  = attributesASN1S(Container Context 2) adAuthAttrs
+        tag = gOctetString (B.convert adMAC)
+        ua  = attributesASN1S (Container Context 3) adUnauthAttrs
+
+        oi | adOriginatorInfo == mempty = id
+           | otherwise = originatorInfoASN1S (Container Context 0) adOriginatorInfo
+
+        v | hasChoiceOther adOriginatorInfo = 3
+          | otherwise                       = 0
+
+instance ParseASN1Object [ASN1Event] AuthenticatedData where
+    parse =
+        onNextContainer Sequence $ do
+            IntVal v <- getNext
+            when (v `notElem` [0, 1, 3]) $
+                throwParseError ("AuthenticatedData: parsed invalid version: " ++ show v)
+            oi <- parseOriginatorInfo (Container Context 0) <|> return mempty
+            ris <- onNextContainer Set parse
+            alg <- parseAlgorithm Sequence
+            dig <- parseAlgorithmMaybe (Container Context 1)
+            inner <- parseEncapsulatedContentInfo
+            aAttrs <- parseAttributes (Container Context 2)
+            OctetString tag <- getNext
+            uAttrs <- parseAttributes (Container Context 3)
+            return AuthenticatedData { adOriginatorInfo = oi
+                                     , adRecipientInfos = ris
+                                     , adMACAlgorithm = alg
+                                     , adDigestAlgorithm = dig
+                                     , adContentInfo = inner
+                                     , adAuthAttrs = aAttrs
+                                     , adMAC = AuthTag $ B.convert tag
+                                     , adUnauthAttrs = uAttrs
+                                     }
+
+
+-- Utilities
+
+decode :: ParseASN1 [ASN1Event] a -> ByteString -> Either StoreError a
+decode parser bs = vals >>= mapLeft ParseFailure . runParseASN1_ parser
+  where vals = mapLeft DecodingError (decodeASN1Repr' BER bs)
+
+-- | Encode the information for encapsulation in another content info.
+encapsulate :: ContentInfo -> ByteString
+encapsulate (DataCI bs)              = bs
+encapsulate (SignedDataCI ed)        = encodeASN1Object ed
+encapsulate (EnvelopedDataCI ed)     = encodeASN1Object ed
+encapsulate (DigestedDataCI dd)      = encodeASN1Object dd
+encapsulate (EncryptedDataCI ed)     = encodeASN1Object ed
+encapsulate (AuthenticatedDataCI ad) = encodeASN1Object ad
+encapsulate (AuthEnvelopedDataCI ae) = encodeASN1Object ae
+
+-- | Decode the information from encapsulated content.
+decapsulate :: ContentType -> ByteString -> Either StoreError ContentInfo
+decapsulate DataType bs              = pure (DataCI bs)
+decapsulate SignedDataType bs        = SignedDataCI <$> decode parse bs
+decapsulate EnvelopedDataType bs     = EnvelopedDataCI <$> decode parse bs
+decapsulate DigestedDataType bs      = DigestedDataCI <$> decode parse bs
+decapsulate EncryptedDataType bs     = EncryptedDataCI <$> decode parse bs
+decapsulate AuthenticatedDataType bs = AuthenticatedDataCI <$> decode parse bs
+decapsulate AuthEnvelopedDataType bs = AuthEnvelopedDataCI <$> decode parse bs
+
+encapsulatedContentInfoASN1S :: ASN1Elem e => ContentInfo -> ASN1Stream e
+encapsulatedContentInfoASN1S ci = asn1Container Sequence (oid . cont)
+  where oid = gOID $ getObjectID $ getContentType ci
+        cont = asn1Container (Container Context 0) inner
+        inner = gOctetString (encapsulate ci)
+
+parseEncapsulatedContentInfo :: Monoid e => ParseASN1 e ContentInfo
+parseEncapsulatedContentInfo =
+    onNextContainer Sequence $ do
+        OID oid <- getNext
+        withObjectID "content type" oid $ \ct ->
+            onNextContainer (Container Context 0) (parseInner ct)
+  where
+    parseInner ct = do
+        bs <- parseContentSingle <|> parseContentChunks
+        case decapsulate ct bs of
+            Left err -> throwParseError
+                ("Unable to decode and parse encapsulated ASN.1: " ++ show err)
+            Right ci -> return ci
+
+    parseContentSingle = do { OctetString bs <- getNext; return bs }
+    parseContentChunks = onNextContainer (Container Universal 4) $
+        B.concat <$> getMany parseContentSingle
diff --git a/src/Crypto/Store/CMS/OriginatorInfo.hs b/src/Crypto/Store/CMS/OriginatorInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/OriginatorInfo.hs
@@ -0,0 +1,185 @@
+-- |
+-- Module      : Crypto.Store.CMS.OriginatorInfo
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.OriginatorInfo
+    ( OriginatorInfo(..)
+    , CertificateChoice(..)
+    , OtherCertificateFormat(..)
+    , RevocationInfoChoice(..)
+    , OtherRevocationInfoFormat(..)
+    , originatorInfoASN1S
+    , parseOriginatorInfo
+    , hasChoiceOther
+    ) where
+
+import Control.Applicative
+
+import Data.ASN1.Types
+import Data.Maybe (fromMaybe)
+import Data.Semigroup
+import Data.X509
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Util
+
+-- | Data types where choice "other" is available.
+class HasChoiceOther a where
+    -- | Return true when choice "other" is selected.
+    hasChoiceOther :: a -> Bool
+
+instance (HasChoiceOther a, Foldable f) => HasChoiceOther (f a) where
+    hasChoiceOther = any hasChoiceOther
+
+-- | Information about the originator of the content info, to be used when
+-- a key management algorithm requires this information.
+data OriginatorInfo = OriginatorInfo
+    { originatorCerts :: [CertificateChoice]
+      -- ^ The collection of certificates
+    , originatorCRLs  :: [RevocationInfoChoice]
+      -- ^ The collection of CRLs
+    }
+    deriving (Show,Eq)
+
+instance Semigroup OriginatorInfo where
+    OriginatorInfo a b <> OriginatorInfo c d = OriginatorInfo (a <> c) (b <> d)
+
+instance Monoid OriginatorInfo where
+    mempty = OriginatorInfo [] []
+    mappend (OriginatorInfo a b) (OriginatorInfo c d) =
+        OriginatorInfo (mappend a c) (mappend b d)
+
+instance HasChoiceOther OriginatorInfo where
+    hasChoiceOther OriginatorInfo{..} =
+        hasChoiceOther originatorCerts || hasChoiceOther originatorCRLs
+
+instance ProduceASN1Object ASN1P OriginatorInfo where
+    asn1s = originatorInfoASN1S Sequence
+
+instance ParseASN1Object [ASN1Event] OriginatorInfo where
+    parse = parseOriginatorInfo Sequence
+
+-- | Generate ASN.1 with the specified constructed type for the originator
+-- information.
+originatorInfoASN1S :: ASN1ConstructionType -> OriginatorInfo -> ASN1PS
+originatorInfoASN1S ty OriginatorInfo{..} =
+    asn1Container ty $ gen 0 originatorCerts . gen 1 originatorCRLs
+  where
+    gen tag list
+        | null list = id
+        | otherwise = asn1Container (Container Context tag) (asn1s list)
+
+-- | Parse originator information with the specified constructed type.
+parseOriginatorInfo :: ASN1ConstructionType
+                    -> ParseASN1 [ASN1Event] OriginatorInfo
+parseOriginatorInfo ty = onNextContainer ty $ do
+    certs <- parseOptList 0
+    crls  <- parseOptList 1
+    return OriginatorInfo { originatorCerts = certs
+                          , originatorCRLs  = crls
+                          }
+  where
+    parseOptList tag =
+        fromMaybe [] <$> onNextContainerMaybe (Container Context tag) parse
+
+-- | Union type related to certificate formats.
+data CertificateChoice
+    = CertificateCertificate SignedCertificate -- ^ X.509 certificate
+    | CertificateOther OtherCertificateFormat  -- ^ Other format
+    deriving (Show,Eq)
+
+instance HasChoiceOther CertificateChoice where
+    hasChoiceOther (CertificateOther _) = True
+    hasChoiceOther _                    = False
+
+instance ProduceASN1Object ASN1P CertificateChoice where
+    asn1s (CertificateCertificate cert) = asn1s cert
+    asn1s (CertificateOther other) =
+        otherCertificateFormatASN1PS (Container Context 3) other
+
+instance ParseASN1Object [ASN1Event] CertificateChoice where
+    parse = parseMain <|> parseOther
+      where parseMain  = CertificateCertificate <$> parse
+            parseOther = CertificateOther <$>
+                parseOtherCertificateFormat (Container Context 3)
+
+-- | Union type related to revocation info formats.
+data RevocationInfoChoice
+    = RevocationInfoCRL SignedCRL
+      -- ^ A CRL, ARL, Delta CRL, or an ACRL
+    | RevocationInfoOther OtherRevocationInfoFormat
+      -- ^ Other format
+    deriving (Show,Eq)
+
+instance HasChoiceOther RevocationInfoChoice where
+    hasChoiceOther (RevocationInfoOther _) = True
+    hasChoiceOther _                       = False
+
+instance ProduceASN1Object ASN1P RevocationInfoChoice where
+    asn1s (RevocationInfoCRL crl) = asn1s crl
+    asn1s (RevocationInfoOther other) =
+        otherRevocationInfoFormatASN1PS (Container Context 1) other
+
+instance ParseASN1Object [ASN1Event] RevocationInfoChoice where
+    parse = parseMain <|> parseOther
+      where parseMain  = RevocationInfoCRL <$> parse
+            parseOther = RevocationInfoOther <$>
+                parseOtherRevocationInfoFormat (Container Context 1)
+
+-- | Certificate information in a format not supported natively.
+data OtherCertificateFormat = OtherCertificateFormat
+    { otherCertFormat :: OID    -- ^ Format identifier
+    , otherCertValues :: [ASN1] -- ^ ASN.1 values using this format
+    }
+    deriving (Show,Eq)
+
+otherCertificateFormatASN1PS :: ASN1Elem e
+                             => ASN1ConstructionType
+                             -> OtherCertificateFormat
+                             -> ASN1Stream e
+otherCertificateFormatASN1PS ty OtherCertificateFormat{..} =
+    asn1Container ty (f . v)
+  where f = gOID otherCertFormat
+        v = gMany otherCertValues
+
+parseOtherCertificateFormat :: Monoid e
+                            => ASN1ConstructionType
+                            -> ParseASN1 e OtherCertificateFormat
+parseOtherCertificateFormat ty = onNextContainer ty $ do
+    OID f <- getNext
+    v <- getMany getNext
+    return OtherCertificateFormat { otherCertFormat = f
+                                  , otherCertValues = v }
+
+-- | Revocation information in a format not supported natively.
+data OtherRevocationInfoFormat = OtherRevocationInfoFormat
+    { otherRevInfoFormat :: OID    -- ^ Format identifier
+    , otherRevInfoValues :: [ASN1] -- ^ ASN.1 values using this format
+    }
+    deriving (Show,Eq)
+
+otherRevocationInfoFormatASN1PS :: ASN1Elem e
+                                => ASN1ConstructionType
+                                -> OtherRevocationInfoFormat
+                                -> ASN1Stream e
+otherRevocationInfoFormatASN1PS ty OtherRevocationInfoFormat{..} =
+    asn1Container ty (f . v)
+  where f = gOID otherRevInfoFormat
+        v = gMany otherRevInfoValues
+
+parseOtherRevocationInfoFormat :: Monoid e
+                               => ASN1ConstructionType
+                               -> ParseASN1 e OtherRevocationInfoFormat
+parseOtherRevocationInfoFormat ty = onNextContainer ty $ do
+    OID f <- getNext
+    v <- getMany getNext
+    return OtherRevocationInfoFormat { otherRevInfoFormat = f
+                                     , otherRevInfoValues = v }
diff --git a/src/Crypto/Store/CMS/PEM.hs b/src/Crypto/Store/CMS/PEM.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/PEM.hs
@@ -0,0 +1,71 @@
+-- |
+-- Module      : Crypto.Store.CMS.PEM
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- PEM serialization and deserialization of CMS 'ContentInfo'.
+module Crypto.Store.CMS.PEM
+    ( readCMSFile
+    , readCMSFileFromMemory
+    , pemToContentInfo
+    , writeCMSFile
+    , writeCMSFileToMemory
+    , contentInfoToPEM
+    ) where
+
+import           Data.ASN1.BinaryEncoding
+import           Data.ASN1.Encoding
+import qualified Data.ByteString as B
+import           Data.Maybe (catMaybes)
+
+import Crypto.Store.CMS.Info
+import Crypto.Store.CMS.Util
+import Crypto.Store.PEM
+
+
+-- Reading from PEM format
+
+-- | Read content info elements from a PEM file.
+readCMSFile :: FilePath -> IO [ContentInfo]
+readCMSFile path = accumulate <$> readPEMs path
+
+-- | Read content info elements from a bytearray in PEM format.
+readCMSFileFromMemory :: B.ByteString -> [ContentInfo]
+readCMSFileFromMemory = either (const []) accumulate . pemParseBS
+
+accumulate :: [PEM] -> [ContentInfo]
+accumulate = catMaybes . foldr (flip pemToContentInfo) []
+
+-- | Read a content info from a 'PEM' element and add it to the accumulator
+-- list.
+pemToContentInfo :: [Maybe ContentInfo] -> PEM -> [Maybe ContentInfo]
+pemToContentInfo acc pem
+    | pemName pem `elem` names = decode (pemContent pem)
+    | otherwise                = Nothing : acc
+  where
+    names = [ "CMS", "PKCS7" ]
+    decode bs =
+        case decodeASN1Repr' BER bs of
+            Left _ -> Nothing : acc
+            Right asn1 ->
+                case fromASN1Repr asn1 of
+                    Right (info, []) -> Just info : acc
+                    _                -> Nothing : acc
+
+
+-- Writing to PEM format
+
+-- | Write content info elements to a PEM file.
+writeCMSFile :: FilePath -> [ContentInfo] -> IO ()
+writeCMSFile path = B.writeFile path . writeCMSFileToMemory
+
+-- | Write content info elements to a bytearray in PEM format.
+writeCMSFileToMemory :: [ContentInfo] -> B.ByteString
+writeCMSFileToMemory = pemsWriteBS . map contentInfoToPEM
+
+-- | Generate PEM for a content info.
+contentInfoToPEM :: ContentInfo -> PEM
+contentInfoToPEM info = PEM { pemName = "CMS", pemHeader = [], pemContent = bs}
+  where bs = encodeASN1Object info
diff --git a/src/Crypto/Store/CMS/Signed.hs b/src/Crypto/Store/CMS/Signed.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Signed.hs
@@ -0,0 +1,240 @@
+-- |
+-- Module      : Crypto.Store.CMS.Signed
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+module Crypto.Store.CMS.Signed
+    ( SignerInfo(..)
+    , SignerIdentifier(..)
+    , IssuerAndSerialNumber(..)
+    , isVersion3
+    , ProducerOfSI
+    , ConsumerOfSI
+    , certSigner
+    , withPublicKey
+    , withSignerKey
+    , withSignerCertificate
+    ) where
+
+import Control.Applicative
+import Control.Monad
+
+import Data.ASN1.Types
+import Data.ByteString (ByteString)
+import Data.List
+import Data.Maybe
+import Data.X509
+
+import Crypto.Random (MonadRandom)
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.AuthEnveloped
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Enveloped
+import Crypto.Store.CMS.OriginatorInfo
+import Crypto.Store.CMS.Type
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+
+-- | Information related to a signer of a 'Crypto.Store.CMS.SignedData'.  An
+-- element contains the signature material that was produced.
+data SignerInfo = SignerInfo
+    { siSignerId :: SignerIdentifier
+      -- ^ Identifier of the signer certificate
+    , siDigestAlgorithm :: DigestAlgorithm
+      -- ^ Digest algorithm used for the signature
+    , siSignedAttrs :: [Attribute]
+      -- ^ Optional signed attributes
+    , siSignatureAlg :: SignatureAlg
+      -- ^ Algorithm used for signature
+    , siSignature :: SignatureValue
+      -- ^ The signature value
+    , siUnsignedAttrs :: [Attribute]
+      -- ^ Optional unsigned attributes
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e SignerInfo where
+    asn1s SignerInfo{..} =
+        asn1Container Sequence (ver . sid . dig . sa . alg . sig . ua)
+      where
+        ver = gIntVal (getVersion siSignerId)
+        sid = asn1s siSignerId
+        dig = algorithmASN1S Sequence siDigestAlgorithm
+        sa  = attributesASN1S (Container Context 0) siSignedAttrs
+        alg = algorithmASN1S Sequence siSignatureAlg
+        sig = gOctetString siSignature
+        ua  = attributesASN1S (Container Context 1) siUnsignedAttrs
+
+instance Monoid e => ParseASN1Object e SignerInfo where
+    parse = onNextContainer Sequence $ do
+        IntVal v <- getNext
+        when (v /= 1 && v /= 3) $
+            throwParseError ("SignerInfo: parsed invalid version: " ++ show v)
+        sid <- parse
+        dig <- parseAlgorithm Sequence
+        sAttrs <- parseAttributes (Container Context 0)
+        alg <- parseAlgorithm Sequence
+        (OctetString sig) <- getNext
+        uAttrs <- parseAttributes (Container Context 1)
+        return SignerInfo { siSignerId = sid
+                          , siDigestAlgorithm = dig
+                          , siSignedAttrs = sAttrs
+                          , siSignatureAlg = alg
+                          , siSignature = sig
+                          , siUnsignedAttrs = uAttrs
+                          }
+
+getVersion :: SignerIdentifier -> Integer
+getVersion (SignerIASN _) = 1
+getVersion (SignerSKI _)  = 3
+
+-- | Return true when the signer info has version 3.
+isVersion3 :: SignerInfo -> Bool
+isVersion3 = (== 3) . getVersion . siSignerId
+
+-- | Union type related to identification of the signer certificate.
+data SignerIdentifier
+    = SignerIASN IssuerAndSerialNumber  -- ^ Issuer and Serial Number
+    | SignerSKI  ByteString             -- ^ Subject Key Identifier
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e SignerIdentifier where
+    asn1s (SignerIASN iasn) = asn1s iasn
+    asn1s (SignerSKI  ski)  = asn1Container (Container Context 0)
+                                  (gOctetString ski)
+
+instance Monoid e => ParseASN1Object e SignerIdentifier where
+    parse = parseIASN <|> parseSKI
+      where parseIASN = SignerIASN <$> parse
+            parseSKI  = SignerSKI  <$>
+                onNextContainer (Container Context 0) parseBS
+            parseBS = do { OctetString bs <- getNext; return bs }
+
+-- | Try to find a certificate with the specified identifier.
+findSigner :: SignerIdentifier
+           -> [SignedCertificate]
+           -> Maybe (SignedCertificate, [SignedCertificate])
+findSigner (SignerIASN iasn) certs =
+    partitionHead (matchIASN . signedObject . getSigned) certs
+  where
+    matchIASN c =
+        (iasnIssuer iasn, iasnSerial iasn) == (certIssuerDN c, certSerial c)
+findSigner (SignerSKI  ski) certs =
+    partitionHead (matchSKI. signedObject . getSigned) certs
+  where
+    matchSKI c =
+        case extensionGet (certExtensions c) of
+            Just (ExtSubjectKeyId idBs) -> idBs == ski
+            Nothing                     -> False
+
+partitionHead :: (a -> Bool) -> [a] -> Maybe (a, [a])
+partitionHead p l =
+    case partition p l of
+        (x : _, r) -> Just (x, r)
+        ([]   , _)    -> Nothing
+
+-- | Function able to produce a 'SignerInfo'.
+type ProducerOfSI m = ContentType -> ByteString -> m (Either StoreError (SignerInfo, [CertificateChoice], [RevocationInfoChoice]))
+
+-- | Function able to consume a 'SignerInfo'.
+type ConsumerOfSI m = ContentType -> ByteString -> SignerInfo -> [CertificateChoice] -> [RevocationInfoChoice] -> m Bool
+
+-- | Create a signer info with the specified signature algorithm and
+-- credentials.
+--
+-- Two lists of optional attributes can be provided.  The attributes will be
+-- part of message signature when provided in the first list.
+--
+-- When the first list of attributes is provided, even empty list, signature is
+-- computed from a digest of the content.  When the list of attributes is
+-- 'Nothing', no intermediate digest is used and the signature is computed from
+-- the full message.
+certSigner :: MonadRandom m
+           => SignatureAlg
+           -> PrivKey
+           -> CertificateChain
+           -> Maybe [Attribute]
+           -> [Attribute]
+           -> ProducerOfSI m
+certSigner alg priv (CertificateChain chain) sAttrsM uAttrs ct msg =
+    fmap build <$> generate
+  where
+    md   = digest dig msg
+    def  = DigestAlgorithm Crypto.Store.CMS.Algorithms.SHA256
+    cert = head chain
+    obj  = signedObject (getSigned cert)
+    isn  = IssuerAndSerialNumber (certIssuerDN obj) (certSerial obj)
+
+    (dig, alg') = signatureResolveHash def alg
+
+    (sAttrs, input) =
+        case sAttrsM of
+            Nothing    -> ([], msg)
+            Just attrs ->
+                let l = setContentTypeAttr ct $ setMessageDigestAttr md attrs
+                 in (l, encodeAuthAttrs l)
+
+    generate  = signatureGenerate alg' priv input
+    build sig =
+        let si = SignerInfo { siSignerId = SignerIASN isn
+                            , siDigestAlgorithm = dig
+                            , siSignedAttrs = sAttrs
+                            , siSignatureAlg = alg
+                            , siSignature = sig
+                            , siUnsignedAttrs = uAttrs
+                            }
+         in (si, map CertificateCertificate chain, [])
+
+-- | Verify that the signature was produced from the specified public key.
+-- Ignores all certificates and CRLs contained in the signed data.
+withPublicKey :: Applicative f => PubKey -> ConsumerOfSI f
+withPublicKey pub ct msg SignerInfo{..} _ _ = pure $
+    fromMaybe False $ do
+        guard (noAttr || attrMatch)
+        alg <- signatureCheckHash siDigestAlgorithm siSignatureAlg
+        return (signatureVerify alg pub input siSignature)
+  where
+    noAttr    = null siSignedAttrs
+    mdMatch   = mdAttr == Just (digest siDigestAlgorithm msg)
+    attrMatch = ctAttr == Just ct && mdMatch
+    mdAttr    = getMessageDigestAttr siSignedAttrs
+    ctAttr    = getContentTypeAttr siSignedAttrs
+    input     = if noAttr then msg else encodeAuthAttrs siSignedAttrs
+
+-- | Verify that the signature is valid with one of the X.509 certificates
+-- contained in the signed data, but does not validate that the certificates are
+-- valid.  All transmitted certificates are implicitely trusted and all CRLs are
+-- ignored.
+withSignerKey :: Applicative f => ConsumerOfSI f
+withSignerKey = withSignerCertificate (\_ -> pure True)
+
+-- | Verify that the signature is valid with one of the X.509 certificates
+-- contained in the signed data, and verify that the signer certificate is valid
+-- using the validation function supplied.  All CRLs are ignored.
+withSignerCertificate :: Applicative f
+                      => (CertificateChain -> f Bool) -> ConsumerOfSI f
+withSignerCertificate validate ct msg SignerInfo{..} certs crls =
+    case getCertificateChain of
+        Just chain -> validate chain
+        Nothing    -> pure False
+  where
+    getCertificateChain = do
+        (cert, others) <- findSigner siSignerId x509Certificates
+        let pub = certPubKey $ signedObject $ getSigned cert
+        validSignature <- withPublicKey pub ct msg SignerInfo{..} certs crls
+        guard validSignature
+        return $ CertificateChain (cert : others)
+
+    x509Certificates = mapMaybe asX509 certs
+
+    asX509 (CertificateCertificate c) = Just c
+    asX509 _                          = Nothing
diff --git a/src/Crypto/Store/CMS/Type.hs b/src/Crypto/Store/CMS/Type.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Type.hs
@@ -0,0 +1,50 @@
+-- |
+-- Module      : Crypto.Store.CMS.Type
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- CMS content information type.
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE TypeFamilies #-}
+module Crypto.Store.CMS.Type
+    ( ContentType(..)
+    ) where
+
+import Data.ASN1.OID
+
+import Crypto.Store.CMS.Util
+
+-- | CMS content information type.
+data ContentType = DataType              -- ^ Arbitrary octet string
+                 | SignedDataType        -- ^ Signed content info
+                 | EnvelopedDataType     -- ^ Enveloped content info
+                 | DigestedDataType      -- ^ Content info with associated digest
+                 | EncryptedDataType     -- ^ Encrypted content info
+                 | AuthenticatedDataType -- ^ Authenticated content info
+                 | AuthEnvelopedDataType -- ^ Authenticated-enveloped content info
+                 deriving (Show,Eq)
+
+instance Enumerable ContentType where
+    values = [ DataType
+             , SignedDataType
+             , EnvelopedDataType
+             , DigestedDataType
+             , EncryptedDataType
+             , AuthenticatedDataType
+             , AuthEnvelopedDataType
+             ]
+
+instance OIDable ContentType where
+    getObjectID DataType              = [1,2,840,113549,1,7,1]
+    getObjectID SignedDataType        = [1,2,840,113549,1,7,2]
+    getObjectID EnvelopedDataType     = [1,2,840,113549,1,7,3]
+    getObjectID DigestedDataType      = [1,2,840,113549,1,7,5]
+    getObjectID EncryptedDataType     = [1,2,840,113549,1,7,6]
+    getObjectID AuthenticatedDataType = [1,2,840,113549,1,9,16,1,2]
+    getObjectID AuthEnvelopedDataType = [1,2,840,113549,1,9,16,1,23]
+
+instance OIDNameable ContentType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
diff --git a/src/Crypto/Store/CMS/Util.hs b/src/Crypto/Store/CMS/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/CMS/Util.hs
@@ -0,0 +1,225 @@
+-- |
+-- Module      : Crypto.Store.CMS.Util
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- CMS and ASN.1 utilities
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+module Crypto.Store.CMS.Util
+    (
+    -- * Testing ASN.1 types
+      nullOrNothing
+    , intOrNothing
+    , dateTimeOrNothing
+    -- * Object Identifiers
+    , OIDTable
+    , lookupOID
+    , Enumerable(..)
+    , OIDNameableWrapper(..)
+    , withObjectID
+    -- * Parsing and encoding ASN.1 objects
+    , ASN1Event
+    , ASN1ObjectExact(..)
+    , ProduceASN1Object(..)
+    , encodeASN1Object
+    , ParseASN1Object(..)
+    , fromASN1Repr
+    -- * Algorithm Identifiers
+    , AlgorithmId(..)
+    , algorithmASN1S
+    , algorithmMaybeASN1S
+    , parseAlgorithm
+    , parseAlgorithmMaybe
+    -- * Miscellaneous functions
+    , orElse
+    ) where
+
+import           Data.ASN1.BinaryEncoding.Raw
+import           Data.ASN1.OID
+import           Data.ASN1.Stream
+import           Data.ASN1.Types
+import           Data.ByteString (ByteString)
+import           Data.List (find)
+import           Data.X509
+
+import Time.Types (DateTime)
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+
+-- | Try to parse a 'Null' ASN.1 value.
+nullOrNothing :: ASN1 -> Maybe ()
+nullOrNothing Null = Just ()
+nullOrNothing _    = Nothing
+
+-- | Try to parse an 'IntVal' ASN.1 value.
+intOrNothing :: ASN1 -> Maybe Integer
+intOrNothing (IntVal i) = Just i
+intOrNothing _          = Nothing
+
+-- | Try to parse a 'DateTime' ASN.1 value.
+dateTimeOrNothing :: ASN1 -> Maybe DateTime
+dateTimeOrNothing (ASN1Time _ t _) = Just t
+dateTimeOrNothing _                = Nothing
+
+-- | Mapping between values and OIDs.
+type OIDTable a = [(a, OID)]
+
+-- | Find the value associated to an OID.
+lookupByOID :: OIDTable a -> OID -> Maybe a
+lookupByOID table oid = fst <$> find ((==) oid . snd) table
+
+-- | Find the OID associated to a value.
+lookupOID :: Eq a => OIDTable a -> a -> Maybe OID
+lookupOID table a = lookup a table
+
+-- | Types with a finite set of values.
+class Enumerable a where
+    -- | Return all possible values for the given type.
+    values :: [a]
+
+-- | Type used to transform a 'Enumerable' instance to an 'OIDNameable'
+-- instance.
+newtype OIDNameableWrapper a = OIDNW { unOIDNW :: a }
+    deriving (Show,Eq)
+
+instance (Enumerable a, OIDable a) => OIDNameable (OIDNameableWrapper a) where
+    fromObjectID = lookupByOID table
+      where table = [ (OIDNW val, getObjectID val) | val <- values ]
+
+-- | Convert the specified OID and apply a parser to the result.
+withObjectID :: OIDNameable a
+             => String -> OID -> (a -> ParseASN1 e b) -> ParseASN1 e b
+withObjectID name oid fn =
+    case fromObjectID oid of
+        Just val -> fn val
+        Nothing  ->
+            throwParseError ("Unsupported " ++ name ++ ": OID " ++ show oid)
+
+-- | Objects that can produce an ASN.1 stream.
+class ProduceASN1Object e obj where
+    asn1s :: obj -> ASN1Stream e
+
+instance ProduceASN1Object e obj => ProduceASN1Object e [obj] where
+    asn1s l r = foldr asn1s r l
+
+instance ASN1Elem e => ProduceASN1Object e DistinguishedName where
+    asn1s = asn1Container Sequence . inner
+      where
+        inner (DistinguishedName dn) cont = foldr dnSet cont dn
+        dnSet (oid, cs) =
+            asn1Container Set $
+                asn1Container Sequence (gOID oid . gASN1String cs)
+
+instance (Show a, Eq a, ASN1Object a) => ProduceASN1Object ASN1P (SignedExact a) where
+    asn1s = gEncoded . encodeSignedObject
+
+-- | Encode the ASN.1 object to DER format.
+encodeASN1Object :: ProduceASN1Object ASN1P obj => obj -> ByteString
+encodeASN1Object = encodeASN1S . asn1s
+
+-- | Objects that can be parsed from an ASN.1 stream.
+class Monoid e => ParseASN1Object e obj where
+    parse :: ParseASN1 e obj
+
+instance ParseASN1Object e obj => ParseASN1Object e [obj] where
+    parse = getMany parse
+
+instance Monoid e => ParseASN1Object e DistinguishedName where
+    parse = DistinguishedName <$> onNextContainer Sequence inner
+      where
+        inner = concat <$> getMany parseOne
+        parseOne =
+            onNextContainer Set $ getMany $
+                onNextContainer Sequence $ do
+                    OID oid <- getNext
+                    ASN1String cs <- getNext
+                    return (oid, cs)
+
+instance (Show a, Eq a, ASN1Object a) => ParseASN1Object [ASN1Event] (SignedExact a) where
+    parse = withAnnotations parseSequence >>= finish
+      where
+        parseSequence = onNextContainer Sequence (getMany getNext)
+        finish (_, events) =
+            case decodeSignedObject (toByteString events) of
+                Right se -> return se
+                Left err -> throwParseError ("SignedExact: " ++ err)
+
+-- | Create an object from the ASN.1 stream.
+fromASN1Repr :: ParseASN1Object [ASN1Event] obj
+             => [ASN1Repr] -> Either String (obj, [ASN1Repr])
+fromASN1Repr = runParseASN1State_ parse
+
+-- | An ASN.1 object associated with the raw data it was parsed from.
+data ASN1ObjectExact a = ASN1ObjectExact
+    { exactObject    :: a           -- ^ The wrapped ASN.1 object
+    , exactObjectRaw :: ByteString  -- ^ The raw representation of this object
+    } deriving Show
+
+instance Eq a => Eq (ASN1ObjectExact a)
+    where a == b = exactObject a == exactObject b
+
+instance ProduceASN1Object ASN1P a => ProduceASN1Object ASN1P (ASN1ObjectExact a) where
+    asn1s = gEncoded . exactObjectRaw
+
+instance ParseASN1Object [ASN1Event] a => ParseASN1Object [ASN1Event] (ASN1ObjectExact a) where
+    parse = do
+        (obj, events) <- withAnnotations parse
+        let objRaw = toByteString events
+        return ASN1ObjectExact { exactObject = obj, exactObjectRaw = objRaw }
+
+-- | Algorithm identifier with associated parameter.
+class AlgorithmId param where
+    type AlgorithmType param
+    algorithmName  :: param -> String
+    algorithmType  :: param -> AlgorithmType param
+    parameterASN1S :: ASN1Elem e => param -> ASN1Stream e
+    parseParameter :: Monoid e => AlgorithmType param -> ParseASN1 e param
+
+-- | Transform the algorithm identifier to ASN.1 stream.
+algorithmASN1S :: (ASN1Elem e, AlgorithmId param, OIDable (AlgorithmType param))
+               => ASN1ConstructionType -> param -> ASN1Stream e
+algorithmASN1S ty p = asn1Container ty (oid . parameterASN1S p)
+  where typ = algorithmType p
+        oid = gOID (getObjectID typ)
+
+-- | Transform the optional algorithm identifier to ASN.1 stream.
+algorithmMaybeASN1S :: (ASN1Elem e, AlgorithmId param, OIDable (AlgorithmType param))
+                    => ASN1ConstructionType -> Maybe param -> ASN1Stream e
+algorithmMaybeASN1S _  Nothing  = id
+algorithmMaybeASN1S ty (Just p) = algorithmASN1S ty p
+
+-- | Parse an algorithm identifier from an ASN.1 stream.
+parseAlgorithm :: forall e param . (Monoid e, AlgorithmId param, OIDNameable (AlgorithmType param))
+               => ASN1ConstructionType -> ParseASN1 e param
+parseAlgorithm ty = onNextContainer ty $ do
+    OID oid <- getNext
+    withObjectID (getName undefined) oid parseParameter
+  where
+    getName :: param -> String
+    getName = algorithmName
+
+-- | Parse an optional algorithm identifier from an ASN.1 stream.
+parseAlgorithmMaybe :: forall e param . (Monoid e, AlgorithmId param, OIDNameable (AlgorithmType param))
+                    => ASN1ConstructionType -> ParseASN1 e (Maybe param)
+parseAlgorithmMaybe ty = onNextContainerMaybe ty $ do
+    OID oid <- getNext
+    withObjectID (getName undefined) oid parseParameter
+  where
+    getName :: param -> String
+    getName = algorithmName
+
+-- | Execute the second action only if the first action produced 'Nothing'.
+orElse :: Monad m => m (Maybe a) -> m (Maybe a) -> m (Maybe a)
+orElse pa pb = do
+    va <- pa
+    case va of
+        Nothing -> pb
+        _       -> return va
diff --git a/src/Crypto/Store/Cipher/RC2.hs b/src/Crypto/Store/Cipher/RC2.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/Cipher/RC2.hs
@@ -0,0 +1,53 @@
+-- |
+-- Module      : Crypto.Store.Cipher.RC2
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : stable
+-- Portability : good
+--
+-- Implementation of RC2 block cipher, a legacy algorithm providing weak
+-- security.  Use only for compatibility with software requiring this cipher and
+-- data which is not sensitive.
+module Crypto.Store.Cipher.RC2
+    ( RC2
+    , rc2WithEffectiveKeyLength
+    ) where
+
+import           Data.ByteArray (ByteArrayAccess)
+import qualified Data.ByteArray as B
+import qualified Data.ByteArray.Mapping as B
+import           Data.Maybe (fromMaybe)
+
+import  Crypto.Error
+import  Crypto.Cipher.Types
+
+import  Crypto.Store.Cipher.RC2.Primitive
+
+-- | RC2 block cipher.  Key is between 8 and 1024 bits.
+newtype RC2 = RC2 Key
+
+instance Cipher RC2 where
+    cipherName    _ = "RC2"
+    cipherKeySize _ = KeySizeRange 1 128
+    cipherInit      = fmap RC2 . initRC2 Nothing
+
+instance BlockCipher RC2 where
+    blockSize _ = 8
+    ecbEncrypt (RC2 k) = B.mapAsWord64 (encrypt k)
+    ecbDecrypt (RC2 k) = B.mapAsWord64 (decrypt k)
+
+-- | Build a RC2 cipher with the specified effective key length (in bits).
+rc2WithEffectiveKeyLength :: ByteArrayAccess key
+                          => Int -> key -> CryptoFailable RC2
+rc2WithEffectiveKeyLength bits key
+    | bits < 1    = CryptoFailed CryptoError_KeySizeInvalid
+    | bits > 1024 = CryptoFailed CryptoError_KeySizeInvalid
+    | otherwise   = RC2 <$> initRC2 (Just bits) key
+
+initRC2 :: ByteArrayAccess key => Maybe Int -> key -> CryptoFailable Key
+initRC2 mbits bs
+    | len <    1 = CryptoFailed CryptoError_KeySizeInvalid
+    | len <= 128 = CryptoPassed (buildKey t1 bs)
+    | otherwise  = CryptoFailed CryptoError_KeySizeInvalid
+  where len = B.length bs
+        t1  = fromMaybe (8 * len) mbits
diff --git a/src/Crypto/Store/Cipher/RC2/Primitive.hs b/src/Crypto/Store/Cipher/RC2/Primitive.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/Cipher/RC2/Primitive.hs
@@ -0,0 +1,238 @@
+-- |
+-- Module      : Crypto.Store.Cipher.RC2.Primitive
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : stable
+-- Portability : good
+--
+{-# LANGUAGE Rank2Types #-}
+module Crypto.Store.Cipher.RC2.Primitive
+    ( Key
+    , buildKey
+    , encrypt
+    , decrypt
+    ) where
+
+import Basement.Block
+import Basement.Compat.IsList
+import Basement.Endianness
+import Basement.Types.OffsetSize
+
+import Control.Monad (forM_)
+
+import           Data.Bits
+import           Data.ByteArray (ByteArrayAccess)
+import qualified Data.ByteArray as B
+import           Data.Word
+
+import Foreign.Storable
+
+
+-- | Expanded RC2 key
+newtype Key = Key (Block Word16) -- [ K[0], K[1], ..., K[63] ]
+
+data Q = Q {-# UNPACK #-} !Word16 {-# UNPACK #-} !Word16
+           {-# UNPACK #-} !Word16 {-# UNPACK #-} !Word16
+
+
+-- Utilities
+
+decomp64 :: Word64 -> Q
+decomp64 x =
+    let f = unBE . toBE . fromIntegral
+        a = f (x `shiftR` 48)
+        b = f (x `shiftR` 32)
+        c = f (x `shiftR` 16)
+        d = f  x
+    in Q a b c d
+
+comp64 :: Q -> Word64
+comp64 (Q a b c d) =
+    (f a `shiftL` 48) .|.
+    (f b `shiftL` 32) .|.
+    (f c `shiftL` 16) .|.
+     f d
+  where f = fromIntegral . unBE . toBE
+
+getR :: Q -> Word8 -> Word16
+getR (Q a b c d) i =
+    case i .&. 3 of
+        0 -> a
+        1 -> b
+        2 -> c
+        _ -> d
+{-# INLINE getR #-}
+
+setR :: Q -> Word8 -> Word16 -> Q
+setR (Q a b c d) i x =
+    case i .&. 3 of
+        0 -> Q x b c d
+        1 -> Q a x c d
+        2 -> Q a b x d
+        _ -> Q a b c x
+{-# INLINE setR #-}
+
+rol :: Word8 -> Word16 -> Word16
+rol i =
+    case i .&. 3 of
+        0 -> flip rotateL 1
+        1 -> flip rotateL 2
+        2 -> flip rotateL 3
+        _ -> flip rotateL 5
+{-# INLINE rol #-}
+
+ror :: Word8 -> Word16 -> Word16
+ror i =
+    case i .&. 3 of
+        0 -> flip rotateR 1
+        1 -> flip rotateR 2
+        2 -> flip rotateR 3
+        _ -> flip rotateR 5
+{-# INLINE ror #-}
+
+f5 :: (a -> a) -> a -> a
+f5 f = f . f . f . f . f
+
+f6 :: (a -> a) -> a -> a
+f6 f = f . f . f . f . f . f
+
+
+-- Encryption
+
+-- | Encrypts a block using the specified key
+encrypt :: Key -> Word64 -> Word64
+encrypt k = comp64 . enc k . decomp64
+
+enc :: Key -> Q -> Q
+enc k r =
+    fst $ f5 (mixingRound k) $ mashingRound k
+        $ f6 (mixingRound k) $ mashingRound k
+        $ f5 (mixingRound k) (r, 0)
+
+-- Decryption
+
+-- | Decrypts a block using the specified key
+decrypt :: Key -> Word64 -> Word64
+decrypt k = comp64 . dec k . decomp64
+
+dec :: Key -> Q -> Q
+dec k r =
+    fst $ f5 (rmixingRound k) $ rmashingRound k
+        $ f6 (rmixingRound k) $ rmashingRound k
+        $ f5 (rmixingRound k) (r, 63)
+
+
+-- Encryptiong rounds
+
+mixUp :: Key -> Word8 -> (Q, Int) -> (Q, Int)
+mixUp k i input@(r, j) = seq r' $ seq j' (r', j')
+  where j' = j + 1
+        r' = setR r i (rol i (ri + gmix k i input))
+        ri = getR r i
+{-# INLINE mixUp #-}
+
+mixingRound :: Key -> (Q, Int) -> (Q, Int)
+mixingRound k = mixUp k 3 . mixUp k 2 . mixUp k 1 . mixUp k 0
+
+mash :: Key -> Word8 -> (Q, Int) -> (Q, Int)
+mash = gmash (+)
+{-# INLINE mash #-}
+
+mashingRound :: Key -> (Q, Int) -> (Q, Int)
+mashingRound k = mash k 3 . mash k 2 . mash k 1 . mash k 0
+
+
+-- Decryption rounds
+
+rmixUp :: Key -> Word8 -> (Q, Int) -> (Q, Int)
+rmixUp k i input@(r, j) = seq r' $ seq j' (r', j')
+  where j' = j - 1
+        r' = setR r i (ri - gmix k i input)
+        ri = ror i (getR r i)
+{-# INLINE rmixUp #-}
+
+rmixingRound :: Key -> (Q, Int) -> (Q, Int)
+rmixingRound k = rmixUp k 0 . rmixUp k 1 . rmixUp k 2 . rmixUp k 3
+
+rmash :: Key -> Word8 -> (Q, Int) -> (Q, Int)
+rmash = gmash (-)
+{-# INLINE rmash #-}
+
+rmashingRound :: Key -> (Q, Int) -> (Q, Int)
+rmashingRound k = rmash k 0 . rmash k 1 . rmash k 2 . rmash k 3
+
+
+-- Generic rounds
+
+gmix :: Key -> Word8 -> (Q, Int) -> Word16
+gmix (Key k) i (r, j) = kj + (ri1 .&. ri2) + (complement ri1 .&. ri3)
+  where ri1 = getR r (i - 1)
+        ri2 = getR r (i - 2)
+        ri3 = getR r (i - 3)
+        kj  = unsafeIndex k (Offset j)
+{-# INLINE gmix #-}
+
+gmash :: (Word16 -> Word16 -> Word16)
+      -> Key -> Word8 -> (Q, Int) -> (Q, Int)
+gmash op (Key k) i (r, j) = seq r' $ seq j (r', j)
+  where r'  = setR r i (ri `op` kp)
+        ri  = getR r i
+        ri1 = getR r (i - 1)
+        kp  = unsafeIndex k $ Offset (fromIntegral ri1 .&. 63)
+{-# INLINE gmash #-}
+
+
+-- Key expansion
+
+-- | Perform key expansion
+buildKey :: ByteArrayAccess key
+         => Int    -- ^ Effective key length in bits
+         -> key    -- ^ Input key between 1 and 128 bytes
+         -> Key    -- ^ Expanded key
+buildKey t1 key = Key $ doCast $ B.allocAndFreeze 128 $ \p -> do
+    B.copyByteArrayToPtr key p
+
+    forM_ [t .. 127] $ \i -> do
+        pos <- (+) <$> peekElemOff p (i - 1) <*> peekElemOff p (i - t)
+        let b = unsafeIndex piTable (fromIntegral pos)
+        pokeElemOff p i b
+
+    pos' <- (.&. tm) <$> peekElemOff p (128 - t8)
+    let b' = unsafeIndex piTable (fromIntegral pos')
+    pokeElemOff p (128 - t8) b'
+
+    forM_ (Prelude.reverse [0 .. 127 - t8]) $ \i -> do
+        pos <- xor <$> peekElemOff p (i + 1) <*> peekElemOff p (i + t8)
+        let b = unsafeIndex piTable (fromIntegral pos)
+        pokeElemOff p i b
+
+  where t  = B.length key
+        t8 = (t1 + 7) `div` 8
+        tm | t1 == 8 * t8 = 255
+           | otherwise    = 255 `mod` shiftL 1 (8 + t1 - 8 * t8)
+
+        doCast :: Block Word8 -> Block Word16
+        doCast = Basement.Block.map fromLE . cast
+
+
+-- PITABLE
+
+piTable :: Block Word8
+piTable = fromList
+    [ 0xd9, 0x78, 0xf9, 0xc4, 0x19, 0xdd, 0xb5, 0xed, 0x28, 0xe9, 0xfd, 0x79, 0x4a, 0xa0, 0xd8, 0x9d
+    , 0xc6, 0x7e, 0x37, 0x83, 0x2b, 0x76, 0x53, 0x8e, 0x62, 0x4c, 0x64, 0x88, 0x44, 0x8b, 0xfb, 0xa2
+    , 0x17, 0x9a, 0x59, 0xf5, 0x87, 0xb3, 0x4f, 0x13, 0x61, 0x45, 0x6d, 0x8d, 0x09, 0x81, 0x7d, 0x32
+    , 0xbd, 0x8f, 0x40, 0xeb, 0x86, 0xb7, 0x7b, 0x0b, 0xf0, 0x95, 0x21, 0x22, 0x5c, 0x6b, 0x4e, 0x82
+    , 0x54, 0xd6, 0x65, 0x93, 0xce, 0x60, 0xb2, 0x1c, 0x73, 0x56, 0xc0, 0x14, 0xa7, 0x8c, 0xf1, 0xdc
+    , 0x12, 0x75, 0xca, 0x1f, 0x3b, 0xbe, 0xe4, 0xd1, 0x42, 0x3d, 0xd4, 0x30, 0xa3, 0x3c, 0xb6, 0x26
+    , 0x6f, 0xbf, 0x0e, 0xda, 0x46, 0x69, 0x07, 0x57, 0x27, 0xf2, 0x1d, 0x9b, 0xbc, 0x94, 0x43, 0x03
+    , 0xf8, 0x11, 0xc7, 0xf6, 0x90, 0xef, 0x3e, 0xe7, 0x06, 0xc3, 0xd5, 0x2f, 0xc8, 0x66, 0x1e, 0xd7
+    , 0x08, 0xe8, 0xea, 0xde, 0x80, 0x52, 0xee, 0xf7, 0x84, 0xaa, 0x72, 0xac, 0x35, 0x4d, 0x6a, 0x2a
+    , 0x96, 0x1a, 0xd2, 0x71, 0x5a, 0x15, 0x49, 0x74, 0x4b, 0x9f, 0xd0, 0x5e, 0x04, 0x18, 0xa4, 0xec
+    , 0xc2, 0xe0, 0x41, 0x6e, 0x0f, 0x51, 0xcb, 0xcc, 0x24, 0x91, 0xaf, 0x50, 0xa1, 0xf4, 0x70, 0x39
+    , 0x99, 0x7c, 0x3a, 0x85, 0x23, 0xb8, 0xb4, 0x7a, 0xfc, 0x02, 0x36, 0x5b, 0x25, 0x55, 0x97, 0x31
+    , 0x2d, 0x5d, 0xfa, 0x98, 0xe3, 0x8a, 0x92, 0xae, 0x05, 0xdf, 0x29, 0x10, 0x67, 0x6c, 0xba, 0xc9
+    , 0xd3, 0x00, 0xe6, 0xcf, 0xe1, 0x9e, 0xa8, 0x2c, 0x63, 0x16, 0x01, 0x3f, 0x58, 0xe2, 0x89, 0xa9
+    , 0x0d, 0x38, 0x34, 0x1b, 0xab, 0x33, 0xff, 0xb0, 0xbb, 0x48, 0x0c, 0x5f, 0xb9, 0xb1, 0xcd, 0x2e
+    , 0xc5, 0xf3, 0xdb, 0x47, 0xe5, 0xa5, 0x9c, 0x77, 0x0a, 0xa6, 0x20, 0x68, 0xfe, 0x7f, 0xc1, 0xad
+    ]
diff --git a/src/Crypto/Store/Error.hs b/src/Crypto/Store/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/Error.hs
@@ -0,0 +1,65 @@
+-- |
+-- Module      : Crypto.Store.Error
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Error data type.
+module Crypto.Store.Error
+    ( StoreError(..)
+    , fromCryptoFailable
+    ) where
+
+import Crypto.Error
+import Crypto.PubKey.RSA.Types as RSA
+
+import Data.ASN1.Error
+
+-- | Error type in cryptostore.
+data StoreError =
+      CryptoError CryptoError
+      -- ^ Wraps a cryptonite error
+    | RSAError RSA.Error
+      -- ^ Wraps an RSA crypto error
+    | DecodingError ASN1Error
+      -- ^ Error while decoding ASN.1 content
+    | ParseFailure String
+      -- ^ Error while parsing an ASN.1 object
+    | DecryptionFailed
+      -- ^ Unable to decrypt, incorrect key or password?
+    | BadContentMAC
+      -- ^ MAC verification failed, incorrect key or password?
+    | BadChecksum
+      -- ^ Checksum verification failed, incorrect key or password?
+    | InvalidInput String
+      -- ^ Some condition is not met about input to algorithm
+    | InvalidPassword String
+      -- ^ Some condition is not met about input password
+    | InvalidParameter String
+      -- ^ Some condition is not met about algorithm parameters
+    | UnexpectedPublicKeyType
+      -- ^ The algorithm expects another public key type
+    | UnexpectedPrivateKeyType
+      -- ^ The algorithm expects another private key type
+    | RecipientTypeMismatch
+      -- ^ Returned when the type of recipient info does not match the consumer
+      -- function
+    | RecipientKeyNotFound
+      -- ^ The certificate provided does not match any encrypted key found
+    | NoRecipientInfoFound
+      -- ^ No recipient info is available in the enveloped data
+    | NoRecipientInfoMatched
+      -- ^ No recipient info could be used with the consumer function
+    | UnsupportedOriginatorFormat
+      -- ^ Only anonymous public key is supported
+    | UnsupportedEllipticCurve
+      -- ^ The elliptic curve used is not supported
+    | NamedCurveRequired
+      -- ^ The algorithm requires a named elliptic curve
+    deriving (Show,Eq)
+
+-- | Turn a 'CryptoFailed' into a 'StoreError'.
+fromCryptoFailable ::CryptoFailable a -> Either StoreError a
+fromCryptoFailable (CryptoPassed a) = Right a
+fromCryptoFailable (CryptoFailed e) = Left (CryptoError e)
diff --git a/src/Crypto/Store/KeyWrap/AES.hs b/src/Crypto/Store/KeyWrap/AES.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/KeyWrap/AES.hs
@@ -0,0 +1,140 @@
+-- |
+-- Module      : Crypto.Store.KeyWrap.AES
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- AES Key Wrap (<https://tools.ietf.org/html/rfc3394 RFC 3394>) and Extended
+-- Key Wrap (<https://tools.ietf.org/html/rfc5649 RFC 5649>)
+--
+-- Should be used with a cipher from module "Crypto.Cipher.AES".
+module Crypto.Store.KeyWrap.AES
+    ( wrap
+    , unwrap
+    , wrapPad
+    , unwrapPad
+    ) where
+
+import           Data.Bits
+import           Data.ByteArray (ByteArray, ByteArrayAccess, Bytes)
+import qualified Data.ByteArray as B
+import           Data.List
+import           Data.Word
+
+import Crypto.Cipher.Types
+
+import Foreign.Storable
+
+import Crypto.Store.Error
+import Crypto.Store.Util
+
+type Chunked ba = [ba]
+type Pair ba = (ba, ba)
+
+-- TODO: should use a low-level AES implementation to reduce allocations
+
+aes' :: (BlockCipher aes, ByteArray ba) => aes -> Pair ba -> ba
+aes' cipher (msb, lsb) = ecbEncrypt cipher (B.append msb lsb)
+
+aes :: (BlockCipher aes, ByteArray ba) => aes -> Pair ba -> Pair ba
+aes cipher = B.splitAt 8 . aes' cipher
+
+aesrev' :: (BlockCipher aes, ByteArray ba) => aes -> ba -> Pair ba
+aesrev' cipher = B.splitAt 8 . ecbDecrypt cipher
+
+aesrev :: (BlockCipher aes, ByteArray ba) => aes -> Pair ba -> Pair ba
+aesrev cipher (msb, lsb) = aesrev' cipher (B.append msb lsb)
+
+wrapc :: (BlockCipher aes, ByteArray ba)
+      => aes -> ba -> Chunked ba -> Chunked ba
+wrapc cipher iiv list = uncurry (:) $ foldl' pass (iiv, list) [0 .. 5]
+  where
+    n = fromIntegral (length list)
+    pass (a, l) j = mapAccumL f a $ zip [n * j + 1 ..] l
+    f a (i, r) =
+        let (msb, lsb) = aes cipher (a, r)
+         in (xorWith msb i, lsb)
+
+unwrapc :: (BlockCipher aes, ByteArray ba)
+        => aes -> Chunked ba -> Either StoreError (ba, Chunked ba)
+unwrapc _      []         = Left (InvalidInput "KeyWrap.AES: input too short")
+unwrapc cipher (iv:list)  = Right (iiv, reverse out)
+  where
+    (iiv, out) = foldl' pass (iv, reverse list) (reverse [0 .. 5])
+    n = fromIntegral (length list)
+    pass (a, l) j = mapAccumL f a $ zip (reverse [n * j + 1 .. n * j + n]) l
+    f a (i, r) = aesrev cipher (xorWith a i, r)
+
+-- | Wrap a key with the specified AES cipher.
+wrap :: (BlockCipher aes, ByteArray ba) => aes -> ba -> Either StoreError ba
+wrap cipher bs = unchunks . wrapc cipher iiv <$> chunks bs
+  where iiv = B.replicate 8 0xA6
+
+-- | Unwrap an encrypted key with the specified AES cipher.
+unwrap :: (BlockCipher aes, ByteArray ba) => aes -> ba -> Either StoreError ba
+unwrap cipher bs = unchunks <$> (check =<< unwrapc cipher =<< chunks bs)
+  where
+    check (iiv, out)
+        | constAllEq 0xA6 iiv = Right out
+        | otherwise           = Left BadChecksum
+
+chunks :: ByteArray ba => ba -> Either StoreError (Chunked ba)
+chunks bs | B.null bs       = Right []
+          | B.length bs < 8 = Left (InvalidInput "KeyWrap.AES: input is not multiple of 8 bytes")
+          | otherwise       = let (a, b) = B.splitAt 8 bs in (a :) <$> chunks b
+
+unchunks :: ByteArray ba => Chunked ba -> ba
+unchunks = B.concat
+
+padMask :: Bytes
+padMask = B.pack [0xA6, 0x59, 0x59, 0xA6, 0x00, 0x00, 0x00, 0x00]
+
+pad :: ByteArray ba => Int -> ba -> Either StoreError (Pair ba)
+pad inlen bs | inlen  == 0 = Left (InvalidInput "KeyWrap.AES: input is empty")
+             | padlen == 8 = Right (aiv, bs)
+             | otherwise   = Right (aiv, bs `B.append` B.zero padlen)
+  where padlen = 8 - mod inlen 8
+        aiv    = xorWith padMask (fromIntegral inlen)
+
+unpad :: ByteArray ba => Int -> Pair ba -> Either StoreError ba
+unpad inlen (aiv, b)
+    | badlen         = Left BadChecksum
+    | constAllEq 0 p = Right bs
+    | otherwise      = Left BadChecksum
+  where aivlen = fromIntegral (unxor padMask aiv)
+        badlen = inlen < aivlen + 8 || inlen >= aivlen + 16
+        (bs, p) = B.splitAt aivlen b
+
+-- | Pad and wrap a key with the specified AES cipher.
+wrapPad :: (BlockCipher aes, ByteArray ba) => aes -> ba -> Either StoreError ba
+wrapPad cipher bs = doWrap =<< pad inlen bs
+  where
+    inlen = B.length bs
+    doWrap (aiv, b)
+        | inlen <= 8 = Right $ aes' cipher (aiv, b)
+        | otherwise  = unchunks . wrapc cipher aiv <$> chunks b
+
+-- | Unwrap and unpad an encrypted key with the specified AES cipher.
+unwrapPad :: (BlockCipher aes, ByteArray ba) => aes -> ba -> Either StoreError ba
+unwrapPad cipher bs = unpad inlen =<< doUnwrap
+  where
+    inlen = B.length bs
+    doUnwrap
+        | inlen == 16 = let (aiv, b) = aesrev' cipher bs in Right (aiv, b)
+        | otherwise   = fmap unchunks <$> (unwrapc cipher =<< chunks bs)
+
+xorWith :: (ByteArrayAccess bin, ByteArray bout) => bin -> Word64 -> bout
+xorWith bs i = B.copyAndFreeze bs $ \dst -> loop dst len i
+  where len = B.length bs
+        loop _ 0 _ = return ()
+        loop _ _ 0 = return () -- return early (constant-time not needed)
+        loop p n j = do
+            b <- peekByteOff p (n - 1)
+            let mask = fromIntegral j :: Word8
+            pokeByteOff p (n - 1) (xor b mask)
+            loop p (n - 1) (shiftR j 8)
+
+unxor :: (ByteArrayAccess bx, ByteArrayAccess by) => bx -> by -> Word64
+unxor x y = foldl' f 0 $ zipWith xor (B.unpack x) (B.unpack y)
+  where f acc z = shiftL acc 8 + fromIntegral z
diff --git a/src/Crypto/Store/KeyWrap/RC2.hs b/src/Crypto/Store/KeyWrap/RC2.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/KeyWrap/RC2.hs
@@ -0,0 +1,91 @@
+-- |
+-- Module      : Crypto.Store.KeyWrap.RC2
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- RC2 Key Wrap (<https://tools.ietf.org/html/rfc3217 RFC 3217>)
+--
+-- Should be used with a cipher from module "Crypto.Store.Cipher.RC2".
+module Crypto.Store.KeyWrap.RC2
+    ( wrap
+    , wrap'
+    , unwrap
+    ) where
+
+import           Data.ByteArray (ByteArray)
+import qualified Data.ByteArray as B
+
+import Crypto.Cipher.Types
+import Crypto.Hash
+import Crypto.Random
+
+import Crypto.Store.Error
+import Crypto.Store.Util
+
+checksum :: ByteArray ba => ba -> ba
+checksum bs = B.convert $ B.takeView (hashWith SHA1 bs) 8
+
+iv4adda22c79e82105 :: B.Bytes
+iv4adda22c79e82105 = B.pack [0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05]
+
+-- | Wrap an RC2 key with the specified RC2 cipher.
+--
+-- Input must be between 0 and 255 bytes.  A fresh IV should be generated
+-- randomly for each invocation.
+wrap :: (MonadRandom m, BlockCipher cipher, ByteArray ba)
+     => cipher -> IV cipher -> ba -> m (Either StoreError ba)
+wrap = wrap' (return . Left) randomPad
+  where randomPad f = fmap (Right . f) . getRandomBytes
+
+-- | Wrap an RC2 key with the specified RC2 cipher, using the given source of
+-- random padding data.
+--
+-- Input must be between 0 and 255 bytes.  A fresh IV should be generated
+-- randomly for each invocation.
+wrap' :: (ByteArray ba, BlockCipher cipher)
+      => (StoreError -> result) -> ((ba -> ba) -> Int -> result)
+      -> cipher -> IV cipher -> ba -> result
+wrap' failure withRandomPad cipher iv cek
+    | inLen < 256 = withRandomPad f padlen
+    | otherwise   = failure
+        (InvalidInput "KeyWrap.RC2: invalid length for content encryption key")
+  where
+    inLen      = B.length cek
+    padlen     = (7 - inLen) `mod` 8
+
+    f pad =
+        let lcek       = B.cons (fromIntegral inLen) cek
+            lcekpad    = B.append lcek pad
+            lcekpadicv = B.append lcekpad (checksum lcekpad)
+            temp1      = cbcEncrypt cipher iv lcekpadicv
+            temp2      = B.append (B.convert iv) temp1
+            temp3      = reverseBytes temp2
+            Just iv'   = makeIV iv4adda22c79e82105
+         in cbcEncrypt cipher iv' temp3
+
+-- | Unwrap an encrypted RC2 key with the specified RC2 cipher.
+unwrap :: (BlockCipher cipher, ByteArray ba)
+       => cipher -> ba -> Either StoreError ba
+unwrap cipher wrapped
+    | inLen <= 16        = invalid
+    | inLen `mod` 8 /= 0 = invalid
+    | checksumPadValid   = Right cek
+    | otherwise          = invalid
+  where
+    inLen            = B.length wrapped
+    Just iv'         = makeIV iv4adda22c79e82105
+    temp3            = cbcDecrypt cipher iv' wrapped
+    temp2            = reverseBytes temp3
+    (ivBs, temp1)    = B.splitAt 8 temp2
+    Just iv          = makeIV ivBs
+    lcekpadicv       = cbcDecrypt cipher iv temp1
+    (lcekpad, icv)   = B.splitAt (inLen - 16) lcekpadicv
+    Just (l, cekpad) = B.uncons lcekpad
+    len              = fromIntegral l
+    padlen           = inLen - 16 - len - 1
+    cek              = B.take len cekpad
+    invalid          = Left BadChecksum
+    checksumPadValid = B.constEq icv (checksum lcekpad)
+                           &&! padlen >=0 &&! padlen < 8
diff --git a/src/Crypto/Store/KeyWrap/TripleDES.hs b/src/Crypto/Store/KeyWrap/TripleDES.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/KeyWrap/TripleDES.hs
@@ -0,0 +1,66 @@
+-- |
+-- Module      : Crypto.Store.KeyWrap.TripleDES
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Triple-DES Key Wrap (<https://tools.ietf.org/html/rfc3217 RFC 3217>)
+--
+-- Should be used with a cipher from module "Crypto.Cipher.TripleDES".
+module Crypto.Store.KeyWrap.TripleDES
+    ( wrap
+    , unwrap
+    ) where
+
+import           Data.ByteArray (ByteArray)
+import qualified Data.ByteArray as B
+
+import Crypto.Cipher.Types
+import Crypto.Hash
+
+import Crypto.Store.Error
+import Crypto.Store.Util
+
+checksum :: ByteArray ba => ba -> ba
+checksum bs = B.convert $ B.takeView (hashWith SHA1 bs) 8
+
+iv4adda22c79e82105 :: B.Bytes
+iv4adda22c79e82105 = B.pack [0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05]
+
+-- | Wrap a Triple-DES key with the specified Triple-DES cipher.
+--
+-- Input must be 24 bytes.  A fresh IV should be generated randomly for each
+-- invocation.
+wrap :: (BlockCipher cipher, ByteArray ba)
+     => cipher -> IV cipher -> ba -> Either StoreError ba
+wrap cipher iv cek
+    | inLen == 24 = Right wrapped
+    | otherwise   = Left
+        (InvalidInput "KeyWrap.TripleDES: invalid length for content encryption key")
+  where
+    inLen    = B.length cek
+    Just iv' = makeIV iv4adda22c79e82105
+    cekicv   = B.append cek (checksum cek)
+    temp1    = cbcEncrypt cipher iv cekicv
+    temp2    = B.append (B.convert iv) temp1
+    temp3    = reverseBytes temp2
+    wrapped  = cbcEncrypt cipher iv' temp3
+
+-- | Unwrap an encrypted Triple-DES key with the specified Triple-DES cipher.
+unwrap :: (BlockCipher cipher, ByteArray ba)
+       => cipher -> ba -> Either StoreError ba
+unwrap cipher wrapped
+    | inLen /= 40                  = invalid
+    | B.constEq icv (checksum cek) = Right cek
+    | otherwise                    = invalid
+  where
+    inLen         = B.length wrapped
+    Just iv'      = makeIV iv4adda22c79e82105
+    temp3         = cbcDecrypt cipher iv' wrapped
+    temp2         = reverseBytes temp3
+    (ivBs, temp1) = B.splitAt 8 temp2
+    Just iv       = makeIV ivBs
+    cekicv        = cbcDecrypt cipher iv temp1
+    (cek, icv)    = B.splitAt 24 cekicv
+    invalid       = Left BadChecksum
diff --git a/src/Crypto/Store/PEM.hs b/src/Crypto/Store/PEM.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PEM.hs
@@ -0,0 +1,35 @@
+-- |
+-- Module      : Crypto.Store.PEM
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Extend module "Data.PEM".
+module Crypto.Store.PEM
+    ( readPEMs
+    , writePEMs
+    , pemsWriteBS
+    , pemsWriteLBS
+    , module Data.PEM
+    ) where
+
+import Data.PEM
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as L
+
+-- | Read a PEM file from disk.
+readPEMs :: FilePath -> IO [PEM]
+readPEMs filepath = either error id . pemParseLBS <$> L.readFile filepath
+
+-- | Convert a list of PEM elements to a bytestring.
+pemsWriteBS :: [PEM] -> B.ByteString
+pemsWriteBS = L.toStrict . pemsWriteLBS
+
+-- | Convert a list of PEM elements to a lazy bytestring.
+pemsWriteLBS :: [PEM] -> L.ByteString
+pemsWriteLBS = L.concat . map pemWriteLBS
+
+-- | Write a PEM file to disk.
+writePEMs :: FilePath -> [PEM] -> IO ()
+writePEMs filepath = L.writeFile filepath . pemsWriteLBS
diff --git a/src/Crypto/Store/PKCS12.hs b/src/Crypto/Store/PKCS12.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PKCS12.hs
@@ -0,0 +1,652 @@
+-- |
+-- Module      : Crypto.Store.PKCS12
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Personal Information Exchange Syntax, aka PKCS #12.
+--
+-- Only password integrity mode and password privacy modes are supported.
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+module Crypto.Store.PKCS12
+    ( IntegrityParams
+    , readP12File
+    , readP12FileFromMemory
+    , writeP12File
+    , writeP12FileToMemory
+    , writeUnprotectedP12File
+    , writeUnprotectedP12FileToMemory
+    -- * PKCS #12 privacy
+    , PKCS12
+    , unPKCS12
+    , unPKCS12'
+    , unencrypted
+    , encrypted
+    -- * PKCS #12 contents and bags
+    , SafeContents(..)
+    , SafeBag
+    , Bag(..)
+    , SafeInfo(..)
+    , CertInfo(..)
+    , CRLInfo(..)
+    , Attribute(..)
+    , getSafeKeys
+    , getAllSafeKeys
+    , getSafeX509Certs
+    , getAllSafeX509Certs
+    , getSafeX509CRLs
+    , getAllSafeX509CRLs
+    -- * PKCS #12 attributes
+    , findAttribute
+    , setAttribute
+    , filterAttributes
+    , getFriendlyName
+    , setFriendlyName
+    , getLocalKeyId
+    , setLocalKeyId
+    -- * Credentials
+    , fromCredential
+    , toCredential
+    -- * Password-based protection
+    , Password
+    , OptProtected(..)
+    , recover
+    , recoverA
+    ) where
+
+import Control.Monad
+
+import           Data.ASN1.Types
+import           Data.ASN1.BinaryEncoding
+import           Data.ASN1.Encoding
+import qualified Data.ByteArray as B
+import qualified Data.ByteString as BS
+import           Data.Maybe (fromMaybe)
+import           Data.Semigroup
+import qualified Data.X509 as X509
+
+import Crypto.Cipher.Types
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+import Crypto.Store.PKCS5
+import Crypto.Store.PKCS5.PBES1
+import Crypto.Store.PKCS8
+
+
+-- Decoding and parsing
+
+-- | Read a PKCS #12 file from disk.
+readP12File :: FilePath -> IO (Either StoreError (OptProtected PKCS12))
+readP12File path = readP12FileFromMemory <$> BS.readFile path
+
+-- | Read a PKCS #12 file from a bytearray in BER format.
+readP12FileFromMemory :: BS.ByteString -> Either StoreError (OptProtected PKCS12)
+readP12FileFromMemory ber = decode ber >>= integrity
+  where
+    integrity PFX{..} =
+        case macData of
+            Nothing -> Unprotected <$> decode authSafeData
+            Just md -> return $ Protected (verify md authSafeData)
+
+    verify MacData{..} content pwdUTF8 =
+        case digAlg of
+            DigestAlgorithm d ->
+                let fn key macAlg bs
+                        | macValue == mac macAlg key bs = decode bs
+                        | otherwise = Left BadContentMAC
+                 in pkcs12mac Left fn d macParams content pwdUTF8
+
+
+-- Generating and encoding
+
+-- | Parameters used for password integrity mode.
+type IntegrityParams = (DigestAlgorithm, PBEParameter)
+
+-- | Write a PKCS #12 file to disk.
+writeP12File :: FilePath
+             -> IntegrityParams -> Password
+             -> PKCS12
+             -> IO (Either StoreError ())
+writeP12File path intp pw aSafe =
+    case writeP12FileToMemory intp pw aSafe of
+        Left e   -> return (Left e)
+        Right bs -> Right <$> BS.writeFile path bs
+
+-- | Write a PKCS #12 file to a bytearray in DER format.
+writeP12FileToMemory :: IntegrityParams -> Password
+                     -> PKCS12
+                     -> Either StoreError BS.ByteString
+writeP12FileToMemory (alg@(DigestAlgorithm hashAlg), pbeParam) pwdUTF8 aSafe =
+    encode <$> protect
+  where
+    content   = encodeASN1Object aSafe
+    encode md = encodeASN1Object PFX { authSafeData = content, macData = Just md }
+
+    protect = pkcs12mac Left fn hashAlg pbeParam content pwdUTF8
+    fn key macAlg bs = Right MacData { digAlg    = alg
+                                     , macValue  = mac macAlg key bs
+                                     , macParams = pbeParam
+                                     }
+
+-- | Write a PKCS #12 file without integrity protection to disk.
+writeUnprotectedP12File :: FilePath -> PKCS12 -> IO ()
+writeUnprotectedP12File path = BS.writeFile path . writeUnprotectedP12FileToMemory
+
+-- | Write a PKCS #12 file without integrity protection to a bytearray in DER
+-- format.
+writeUnprotectedP12FileToMemory :: PKCS12 -> BS.ByteString
+writeUnprotectedP12FileToMemory aSafe = encodeASN1Object pfx
+  where
+    content = encodeASN1Object aSafe
+    pfx     = PFX { authSafeData = content, macData = Nothing }
+
+
+-- PFX and MacData
+
+data PFX = PFX
+    { authSafeData :: BS.ByteString
+    , macData :: Maybe MacData
+    }
+    deriving (Show,Eq)
+
+instance ProduceASN1Object ASN1P PFX where
+    asn1s PFX{..} =
+        asn1Container Sequence (v . a . m)
+      where
+        v = gIntVal 3
+        a = asn1s (DataCI authSafeData)
+        m = optASN1S macData asn1s
+
+instance ParseASN1Object [ASN1Event] PFX where
+    parse = onNextContainer Sequence $ do
+        IntVal v <- getNext
+        when (v /= 3) $
+            throwParseError ("PFX: parsed invalid version: " ++ show v)
+        ci <- parse
+        d <- case ci of
+                 DataCI bs      -> return bs
+                 SignedDataCI _ -> throwParseError "PFX: public-key integrity mode is not supported"
+                 _              -> throwParseError $ "PFX: invalid content type: " ++ show (getContentType ci)
+        b <- hasNext
+        m <- if b then Just <$> parse else pure Nothing
+        return PFX { authSafeData = d, macData = m }
+
+data MacData = MacData
+    { digAlg :: DigestAlgorithm
+    , macValue :: MessageAuthenticationCode
+    , macParams :: PBEParameter
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e MacData where
+    asn1s MacData{..} =
+        asn1Container Sequence (m . s . i)
+      where
+        m = asn1Container Sequence (a . v)
+        a = algorithmASN1S Sequence digAlg
+        v = gOctetString (B.convert macValue)
+        s = gOctetString (pbeSalt macParams)
+        i = gIntVal (fromIntegral $ pbeIterationCount macParams)
+
+instance Monoid e => ParseASN1Object e MacData where
+    parse = onNextContainer Sequence $ do
+        (a, v) <- onNextContainer Sequence $ do
+            a <- parseAlgorithm Sequence
+            OctetString v <- getNext
+            return (a, v)
+        OctetString s <- getNext
+        b <- hasNext
+        IntVal i <- if b then getNext else pure (IntVal 1)
+        return MacData { digAlg = a
+                       , macValue = AuthTag (B.convert v)
+                       , macParams = PBEParameter s (fromIntegral i)
+                       }
+
+
+-- AuthenticatedSafe
+
+-- | PKCS #12 privacy wrapper, adding optional encryption to 'SafeContents'.
+-- ASN.1 equivalent is @AuthenticatedSafe@.
+--
+-- The semigroup interface allows to combine multiple pieces encrypted
+-- separately but they should all derive from the same password to be readable
+-- by 'unPKCS12' and most other software.
+newtype PKCS12 = PKCS12 [ASElement]
+    deriving (Show,Eq)
+
+instance Semigroup PKCS12 where
+    PKCS12 a <> PKCS12 b = PKCS12 (a ++ b)
+
+instance ProduceASN1Object ASN1P PKCS12 where
+    asn1s (PKCS12 elems) = asn1Container Sequence (asn1s elems)
+
+instance ParseASN1Object [ASN1Event] PKCS12 where
+    parse = PKCS12 <$> onNextContainer Sequence parse
+
+-- | Read the contents of a PKCS #12.  The same privacy password will be used
+-- for all content elements.
+--
+-- This convenience function returns a 'Protected' value as soon as one element
+-- at least is encrypted.  This does not mean all elements were actually
+-- protected in the input.  If detailed view is required then function
+-- 'unPKCS12'' is also available.
+unPKCS12 :: PKCS12 -> OptProtected [SafeContents]
+unPKCS12 = applySamePassword . unPKCS12'
+
+-- | Read the contents of a PKCS #12.
+unPKCS12' :: PKCS12 -> [OptProtected SafeContents]
+unPKCS12' (PKCS12 elems) = map f elems
+  where f (Unencrypted sc) = Unprotected sc
+        f (Encrypted e)    = Protected (decrypt e >=> decode)
+
+-- | Build a PKCS #12 without encryption.  Usage scenario is when private keys
+-- are already encrypted with 'PKCS8ShroudedKeyBag'.
+unencrypted :: SafeContents -> PKCS12
+unencrypted = PKCS12 . (:[]) . Unencrypted
+
+-- | Build a PKCS #12 encrypted with the specified scheme and password.
+encrypted :: EncryptionScheme -> Password -> SafeContents -> Either StoreError PKCS12
+encrypted alg pwd sc = PKCS12 . (:[]) . Encrypted <$> encrypt alg pwd bs
+  where bs = encodeASN1Object sc
+
+data ASElement = Unencrypted SafeContents
+               | Encrypted PKCS5
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e ASElement where
+    asn1s (Unencrypted sc) = asn1Container Sequence (oid . cont)
+      where
+        oid = gOID (getObjectID DataType)
+        cont = asn1Container (Container Context 0) (gOctetString bs)
+        bs = encodeASN1Object sc
+
+    asn1s (Encrypted PKCS5{..}) = asn1Container Sequence (oid . cont)
+      where
+        oid = gOID (getObjectID EncryptedDataType)
+        cont = asn1Container (Container Context 0) inner
+        inner = asn1Container Sequence (gIntVal 0 . eci)
+        eci = encryptedContentInfoASN1S
+                  (DataType, encryptionAlgorithm, encryptedData)
+
+instance Monoid e => ParseASN1Object e ASElement where
+    parse = onNextContainer Sequence $ do
+        OID oid <- getNext
+        withObjectID "content type" oid $ \ct ->
+            onNextContainer (Container Context 0) (parseInner ct)
+      where
+        parseInner DataType          = Unencrypted <$> parseUnencrypted
+        parseInner EncryptedDataType = Encrypted <$> parseEncrypted
+        parseInner EnvelopedDataType = throwParseError "PKCS12: public-key privacy mode is not supported"
+        parseInner ct                = throwParseError $ "PKCS12: invalid content type: " ++ show ct
+
+        parseUnencrypted = parseOctetStringObject "PKCS12"
+        parseEncrypted = onNextContainer Sequence $ do
+            IntVal 0 <- getNext
+            (DataType, eScheme, ed) <- parseEncryptedContentInfo
+            return PKCS5 { encryptionAlgorithm = eScheme, encryptedData = ed }
+
+
+-- Bags
+
+-- | Polymorphic PKCS #12 bag parameterized by the payload data type.
+data Bag info = Bag
+    { bagInfo :: info              -- ^ bag payload
+    , bagAttributes :: [Attribute] -- ^ attributes providing additional information
+    }
+    deriving (Show,Eq)
+
+class BagInfo info where
+    type BagType info
+    bagName  :: info -> String
+    bagType  :: info -> BagType info
+    valueASN1S :: ASN1Elem e => info -> ASN1Stream e
+    parseValue :: Monoid e => BagType info -> ParseASN1 e info
+
+instance (ASN1Elem e, BagInfo info, OIDable (BagType info)) => ProduceASN1Object e (Bag info) where
+    asn1s Bag{..} = asn1Container Sequence (oid . val . att)
+      where
+        typ = bagType bagInfo
+        oid = gOID (getObjectID typ)
+        val = asn1Container (Container Context 0) (valueASN1S bagInfo)
+
+        att | null bagAttributes = id
+            | otherwise          = asn1Container Set (asn1s bagAttributes)
+
+instance (Monoid e, BagInfo info, OIDNameable (BagType info)) => ParseASN1Object e (Bag info) where
+    parse = onNextContainer Sequence $ do
+        OID oid <- getNext
+        val <- withObjectID (getName undefined) oid $
+                   onNextContainer (Container Context 0) . parseValue
+        att <- fromMaybe [] <$> onNextContainerMaybe Set parse
+        return Bag { bagInfo = val, bagAttributes = att }
+      where
+        getName :: info -> String
+        getName = bagName
+
+data CertType = TypeCertX509 deriving (Show,Eq)
+
+instance Enumerable CertType where
+    values = [ TypeCertX509 ]
+
+instance OIDable CertType where
+    getObjectID TypeCertX509 = [1,2,840,113549,1,9,22,1]
+
+instance OIDNameable CertType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Certificate bags.  Only X.509 certificates are supported.
+newtype CertInfo = CertX509 X509.SignedCertificate deriving (Show,Eq)
+
+instance BagInfo CertInfo where
+    type BagType CertInfo = CertType
+    bagName _ = "CertBag"
+    bagType (CertX509 _) = TypeCertX509
+    valueASN1S (CertX509 c) = gOctetString (encodeASN1Object c)
+    parseValue TypeCertX509 = CertX509 <$> parseOctetStringObject "CertBag"
+
+data CRLType = TypeCRLX509 deriving (Show,Eq)
+
+instance Enumerable CRLType where
+    values = [ TypeCRLX509 ]
+
+instance OIDable CRLType where
+    getObjectID TypeCRLX509 = [1,2,840,113549,1,9,23,1]
+
+instance OIDNameable CRLType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | CRL bags.  Only X.509 CRLs are supported.
+newtype CRLInfo = CRLX509 X509.SignedCRL deriving (Show,Eq)
+
+instance BagInfo CRLInfo where
+    type BagType CRLInfo = CRLType
+    bagName _ = "CRLBag"
+    bagType (CRLX509 _) = TypeCRLX509
+    valueASN1S (CRLX509 c) = gOctetString (encodeASN1Object c)
+    parseValue TypeCRLX509 = CRLX509 <$> parseOctetStringObject "CRLBag"
+
+data SafeType = TypeKey
+              | TypePKCS8ShroudedKey
+              | TypeCert
+              | TypeCRL
+              | TypeSecret
+              | TypeSafeContents
+    deriving (Show,Eq)
+
+instance Enumerable SafeType where
+    values = [ TypeKey
+             , TypePKCS8ShroudedKey
+             , TypeCert
+             , TypeCRL
+             , TypeSecret
+             , TypeSafeContents
+             ]
+
+instance OIDable SafeType where
+    getObjectID TypeKey              = [1,2,840,113549,1,12,10,1,1]
+    getObjectID TypePKCS8ShroudedKey = [1,2,840,113549,1,12,10,1,2]
+    getObjectID TypeCert             = [1,2,840,113549,1,12,10,1,3]
+    getObjectID TypeCRL              = [1,2,840,113549,1,12,10,1,4]
+    getObjectID TypeSecret           = [1,2,840,113549,1,12,10,1,5]
+    getObjectID TypeSafeContents     = [1,2,840,113549,1,12,10,1,6]
+
+instance OIDNameable SafeType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Main bag payload in PKCS #12 contents.
+data SafeInfo = KeyBag (FormattedKey X509.PrivKey) -- ^ unencrypted private key
+              | PKCS8ShroudedKeyBag PKCS5          -- ^ encrypted private key
+              | CertBag (Bag CertInfo)             -- ^ certificate
+              | CRLBag (Bag CRLInfo)               -- ^ CRL
+              | SecretBag [ASN1]                   -- ^ arbitrary secret
+              | SafeContentsBag SafeContents       -- ^ safe contents embeded recursively
+    deriving (Show,Eq)
+
+instance BagInfo SafeInfo where
+    type BagType SafeInfo = SafeType
+    bagName _ = "SafeBag"
+
+    bagType (KeyBag _)              = TypeKey
+    bagType (PKCS8ShroudedKeyBag _) = TypePKCS8ShroudedKey
+    bagType (CertBag _)             = TypeCert
+    bagType (CRLBag _)              = TypeCRL
+    bagType (SecretBag _)           = TypeSecret
+    bagType (SafeContentsBag _)     = TypeSafeContents
+
+    valueASN1S (KeyBag k)              = asn1s k
+    valueASN1S (PKCS8ShroudedKeyBag k) = asn1s k
+    valueASN1S (CertBag c)             = asn1s c
+    valueASN1S (CRLBag c)              = asn1s c
+    valueASN1S (SecretBag s)           = gMany s
+    valueASN1S (SafeContentsBag sc)    = asn1s sc
+
+    parseValue TypeKey              = KeyBag <$> parse
+    parseValue TypePKCS8ShroudedKey = PKCS8ShroudedKeyBag <$> parse
+    parseValue TypeCert             = CertBag <$> parse
+    parseValue TypeCRL              = CRLBag <$> parse
+    parseValue TypeSecret           = SecretBag <$> getMany getNext
+    parseValue TypeSafeContents     = SafeContentsBag <$> parse
+
+-- | Main bag type in a PKCS #12.
+type SafeBag = Bag SafeInfo
+
+-- | Content objects stored in a PKCS #12.
+newtype SafeContents = SafeContents { unSafeContents :: [SafeBag] }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e SafeContents where
+    asn1s (SafeContents s) = asn1Container Sequence (asn1s s)
+
+instance Monoid e => ParseASN1Object e SafeContents where
+    parse = SafeContents <$> onNextContainer Sequence parse
+
+-- | Return all private keys contained in the safe contents.
+getSafeKeys :: SafeContents -> [OptProtected X509.PrivKey]
+getSafeKeys (SafeContents scs) = loop scs
+  where
+    loop []           = []
+    loop (bag : bags) =
+        case bagInfo bag of
+            KeyBag (FormattedKey _ k) -> Unprotected k : loop bags
+            PKCS8ShroudedKeyBag k     -> Protected (unshroud k) : loop bags
+            SafeContentsBag inner     -> getSafeKeys inner ++ loop bags
+            _                         -> loop bags
+
+    unshroud shrouded pwd = do
+        bs <- decrypt shrouded pwd
+        FormattedKey _ k <- decode bs
+        return k
+
+-- | Return all private keys contained in the safe content list.  All shrouded
+-- private keys must derive from the same password.
+--
+-- This convenience function returns a 'Protected' value as soon as one key at
+-- least is encrypted.  This does not mean all keys were actually protected in
+-- the input.  If detailed view is required then function 'getSafeKeys' is
+-- available.
+getAllSafeKeys :: [SafeContents] -> OptProtected [X509.PrivKey]
+getAllSafeKeys = applySamePassword . concatMap getSafeKeys
+
+-- | Return all X.509 certificates contained in the safe contents.
+getSafeX509Certs :: SafeContents -> [X509.SignedCertificate]
+getSafeX509Certs (SafeContents scs) = loop scs
+  where
+    loop []           = []
+    loop (bag : bags) =
+        case bagInfo bag of
+            CertBag (Bag (CertX509 c) _) -> c : loop bags
+            SafeContentsBag inner        -> getSafeX509Certs inner ++ loop bags
+            _                            -> loop bags
+
+-- | Return all X.509 certificates contained in the safe content list.
+getAllSafeX509Certs :: [SafeContents] -> [X509.SignedCertificate]
+getAllSafeX509Certs = concatMap getSafeX509Certs
+
+-- | Return all X.509 CRLs contained in the safe contents.
+getSafeX509CRLs :: SafeContents -> [X509.SignedCRL]
+getSafeX509CRLs (SafeContents scs) = loop scs
+  where
+    loop []           = []
+    loop (bag : bags) =
+        case bagInfo bag of
+            CRLBag (Bag (CRLX509 c) _) -> c : loop bags
+            SafeContentsBag inner      -> getSafeX509CRLs inner ++ loop bags
+            _                          -> loop bags
+
+-- | Return all X.509 CRLs contained in the safe content list.
+getAllSafeX509CRLs :: [SafeContents] -> [X509.SignedCRL]
+getAllSafeX509CRLs = concatMap getSafeX509CRLs
+
+
+-- Conversion to/from credentials
+
+getInnerCredential :: [SafeContents] -> SamePassword (Maybe (X509.CertificateChain, X509.PrivKey))
+getInnerCredential l = SamePassword (fn <$> getAllSafeKeys l)
+  where
+    certs = getAllSafeX509Certs l
+
+    fn []               = Nothing
+    fn [k] | null certs = Nothing
+           | otherwise  = Just (X509.CertificateChain certs, k)
+    fn _                = Nothing
+
+-- | Extract the private key and certificate chain from a 'PKCS12' value.  A
+-- credential is returned when the structure contains exactly one private key
+-- and at least one X.509 certificate.
+toCredential :: PKCS12 -> OptProtected (Maybe (X509.CertificateChain, X509.PrivKey))
+toCredential p12 =
+    unSamePassword (SamePassword (unPKCS12 p12) >>= getInnerCredential)
+
+-- | Build a 'PKCS12' value containing a private key and certificate chain.
+-- Distinct encryption is applied for both.  Encrypting the certificate chain is
+-- optional.
+--
+-- Note: advice is to always generate fresh and independent 'EncryptionScheme'
+-- values so that the salt is not reused twice in the encryption process.
+fromCredential :: Maybe EncryptionScheme -- for certificates
+               -> EncryptionScheme       -- for private key
+               -> Password
+               -> (X509.CertificateChain, X509.PrivKey)
+               -> Either StoreError PKCS12
+fromCredential algChain algKey pwd (X509.CertificateChain certs, key)
+    | null certs = Left (InvalidInput "Empty certificate chain")
+    | otherwise  = (<>) <$> pkcs12Chain <*> pkcs12Key
+  where
+    pkcs12Key   = unencrypted <$> scKeyOrError
+    pkcs12Chain =
+        case algChain of
+            Just alg -> encrypted alg pwd scChain
+            Nothing  -> Right (unencrypted scChain)
+
+    scChain     = SafeContents (map toCertBag certs)
+    toCertBag c = Bag (CertBag (Bag (CertX509 c) [])) []
+
+    scKeyOrError = wrap <$> encrypt algKey pwd encodedKey
+
+    wrap shrouded = SafeContents [Bag (PKCS8ShroudedKeyBag shrouded) []]
+    encodedKey    = encodeASN1Object (FormattedKey PKCS8Format key)
+
+
+-- Standard attributes
+
+friendlyName :: OID
+friendlyName = [1,2,840,113549,1,9,20]
+
+-- | Return the value of the @friendlyName@ attribute.
+getFriendlyName :: [Attribute] -> Maybe String
+getFriendlyName attrs = runParseAttribute friendlyName attrs $ do
+    ASN1String str <- getNext
+    case asn1CharacterToString str of
+        Nothing -> throwParseError "Invalid friendlyName value"
+        Just s  -> return s
+
+-- | Add or replace the @friendlyName@ attribute in a list of attributes.
+setFriendlyName :: String -> [Attribute] -> [Attribute]
+setFriendlyName name = setAttributeASN1S friendlyName (gBMPString name)
+
+localKeyId :: OID
+localKeyId = [1,2,840,113549,1,9,21]
+
+-- | Return the value of the @localKeyId@ attribute.
+getLocalKeyId :: [Attribute] -> Maybe BS.ByteString
+getLocalKeyId attrs = runParseAttribute localKeyId attrs $ do
+    OctetString d <- getNext
+    return d
+
+-- | Add or replace the @localKeyId@ attribute in a list of attributes.
+setLocalKeyId :: BS.ByteString -> [Attribute] -> [Attribute]
+setLocalKeyId d = setAttributeASN1S localKeyId (gOctetString d)
+
+
+-- Utilities
+
+-- Internal wrapper of OptProtected providing Applicative and Monad instances.
+--
+-- This adds the following constraint: all values composed must derive from the
+-- same encryption password.  Semantically, 'Protected' actually means
+-- "requiring a password".  Otherwise composition of 'Protected' and
+-- 'Unprotected' values is unsound.
+newtype SamePassword a = SamePassword { unSamePassword :: OptProtected a }
+
+instance Functor SamePassword where
+    fmap f (SamePassword opt) = SamePassword (fmap f opt)
+
+instance Applicative SamePassword where
+    pure a = SamePassword (Unprotected a)
+
+    SamePassword (Unprotected f) <*> SamePassword (Unprotected x) =
+        SamePassword (Unprotected (f x))
+
+    SamePassword (Unprotected f) <*> SamePassword (Protected x) =
+        SamePassword $ Protected (fmap f . x)
+
+    SamePassword (Protected f) <*> SamePassword (Unprotected x) =
+        SamePassword $ Protected (fmap ($ x) . f)
+
+    SamePassword (Protected f) <*> SamePassword (Protected x) =
+        SamePassword $ Protected (\pwd -> f pwd <*> x pwd)
+
+instance Monad SamePassword where
+    return = pure
+
+    SamePassword (Unprotected x)   >>= f = f x
+    SamePassword (Protected inner) >>= f =
+        SamePassword . Protected $ \pwd ->
+            case inner pwd of
+                Left err -> Left err
+                Right x  -> recover pwd (unSamePassword $ f x)
+
+applySamePassword :: [OptProtected a] -> OptProtected [a]
+applySamePassword = unSamePassword . traverse SamePassword
+
+decode :: ParseASN1Object [ASN1Event] obj => BS.ByteString -> Either StoreError obj
+decode bs =
+    case decodeASN1Repr' BER bs of
+        Left e     -> Left (DecodingError e)
+        Right asn1 ->
+            case fromASN1Repr asn1 of
+                Right (obj, []) -> Right obj
+                Right _         -> Left (ParseFailure "Incomplete parse")
+                Left e          -> Left (ParseFailure e)
+
+parseOctetStringObject :: (Monoid e, ParseASN1Object [ASN1Event] obj)
+                       => String -> ParseASN1 e obj
+parseOctetStringObject name = do
+    OctetString bs <- getNext
+    case decode bs of
+        Left e  -> throwParseError (name ++ ": " ++ show e)
+        Right c -> return c
diff --git a/src/Crypto/Store/PKCS5.hs b/src/Crypto/Store/PKCS5.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PKCS5.hs
@@ -0,0 +1,238 @@
+-- |
+-- Module      : Data.Store.PKCS5
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Password-Based Cryptography, aka PKCS #5.
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+module Crypto.Store.PKCS5
+    ( Password
+    , EncryptedContent
+    -- * High-level API
+    , PKCS5(..)
+    , encrypt
+    , decrypt
+    -- * Encryption schemes
+    , EncryptionScheme(..)
+    , PBEParameter(..)
+    , PBES2Parameter(..)
+    -- * Key derivation
+    , KeyDerivationFunc(..)
+    , PBKDF2_PRF(..)
+    , Salt
+    , generateSalt
+    -- * Content encryption
+    , ContentEncryptionParams
+    , ContentEncryptionAlg(..)
+    , ContentEncryptionCipher(..)
+    , generateEncryptionParams
+    , getContentEncryptionAlg
+    -- * Low-level API
+    , pbEncrypt
+    , pbDecrypt
+    ) where
+
+import           Data.ASN1.Types
+import           Data.ByteArray (ByteArrayAccess)
+import           Data.ByteString (ByteString)
+import           Data.Maybe (fromMaybe)
+
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Encrypted
+import Crypto.Store.CMS.Enveloped
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+import Crypto.Store.PKCS5.PBES1
+
+data EncryptionSchemeType = Type_PBES2
+                          | Type_PBE_MD5_DES_CBC
+                          | Type_PBE_SHA1_DES_CBC
+                          | Type_PBE_SHA1_RC4_128
+                          | Type_PBE_SHA1_RC4_40
+                          | Type_PBE_SHA1_DES_EDE3_CBC
+                          | Type_PBE_SHA1_DES_EDE2_CBC
+                          | Type_PBE_SHA1_RC2_128
+                          | Type_PBE_SHA1_RC2_40
+
+instance Enumerable EncryptionSchemeType where
+    values = [ Type_PBES2
+             , Type_PBE_MD5_DES_CBC
+             , Type_PBE_SHA1_DES_CBC
+             , Type_PBE_SHA1_RC4_128
+             , Type_PBE_SHA1_RC4_40
+             , Type_PBE_SHA1_DES_EDE3_CBC
+             , Type_PBE_SHA1_DES_EDE2_CBC
+             , Type_PBE_SHA1_RC2_128
+             , Type_PBE_SHA1_RC2_40
+             ]
+
+instance OIDable EncryptionSchemeType where
+    getObjectID Type_PBES2                 = [1,2,840,113549,1,5,13]
+    getObjectID Type_PBE_MD5_DES_CBC       = [1,2,840,113549,1,5,3]
+    getObjectID Type_PBE_SHA1_DES_CBC      = [1,2,840,113549,1,5,10]
+    getObjectID Type_PBE_SHA1_RC4_128      = [1,2,840,113549,1,12,1,1]
+    getObjectID Type_PBE_SHA1_RC4_40       = [1,2,840,113549,1,12,1,2]
+    getObjectID Type_PBE_SHA1_DES_EDE3_CBC = [1,2,840,113549,1,12,1,3]
+    getObjectID Type_PBE_SHA1_DES_EDE2_CBC = [1,2,840,113549,1,12,1,4]
+    getObjectID Type_PBE_SHA1_RC2_128      = [1,2,840,113549,1,12,1,5]
+    getObjectID Type_PBE_SHA1_RC2_40       = [1,2,840,113549,1,12,1,6]
+
+instance OIDNameable EncryptionSchemeType where
+    fromObjectID oid = unOIDNW <$> fromObjectID oid
+
+-- | Password-Based Encryption Scheme (PBES).
+data EncryptionScheme = PBES2 PBES2Parameter               -- ^ PBES2
+                      | PBE_MD5_DES_CBC PBEParameter       -- ^ pbeWithMD5AndDES-CBC
+                      | PBE_SHA1_DES_CBC PBEParameter      -- ^ pbeWithSHA1AndDES-CBC
+                      | PBE_SHA1_RC4_128 PBEParameter      -- ^ pbeWithSHAAnd128BitRC4
+                      | PBE_SHA1_RC4_40 PBEParameter       -- ^ pbeWithSHAAnd40BitRC4
+                      | PBE_SHA1_DES_EDE3_CBC PBEParameter -- ^ pbeWithSHAAnd3-KeyTripleDES-CBC
+                      | PBE_SHA1_DES_EDE2_CBC PBEParameter -- ^ pbeWithSHAAnd2-KeyTripleDES-CBC
+                      | PBE_SHA1_RC2_128 PBEParameter      -- ^ pbeWithSHAAnd128BitRC2-CBC
+                      | PBE_SHA1_RC2_40 PBEParameter       -- ^ pbewithSHAAnd40BitRC2-CBC
+                      deriving (Show,Eq)
+
+-- | PBES2 parameters.
+data PBES2Parameter = PBES2Parameter
+    { pbes2KDF     :: KeyDerivationFunc       -- ^ Key derivation function
+    , pbes2EScheme :: ContentEncryptionParams -- ^ Underlying encryption scheme
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e PBES2Parameter where
+    asn1s PBES2Parameter{..} =
+        let kdFunc  = algorithmASN1S Sequence pbes2KDF
+            eScheme = asn1s pbes2EScheme
+         in asn1Container Sequence (kdFunc . eScheme)
+
+instance Monoid e => ParseASN1Object e PBES2Parameter where
+    parse = onNextContainer Sequence $ do
+        kdFunc  <- parseAlgorithm Sequence
+        eScheme <- parse
+        case kdfKeyLength kdFunc of
+            Nothing -> return ()
+            Just sz
+                | validateKeySize eScheme sz -> return ()
+                | otherwise -> throwParseError "PBES2Parameter: parsed key length incompatible with encryption scheme"
+        return PBES2Parameter { pbes2KDF = kdFunc, pbes2EScheme = eScheme }
+
+instance AlgorithmId EncryptionScheme where
+    type AlgorithmType EncryptionScheme = EncryptionSchemeType
+    algorithmName _  = "encryption scheme"
+
+    algorithmType (PBES2 _)                 = Type_PBES2
+    algorithmType (PBE_MD5_DES_CBC _)       = Type_PBE_MD5_DES_CBC
+    algorithmType (PBE_SHA1_DES_CBC _)      = Type_PBE_SHA1_DES_CBC
+    algorithmType (PBE_SHA1_RC4_128 _)      = Type_PBE_SHA1_RC4_128
+    algorithmType (PBE_SHA1_RC4_40 _)       = Type_PBE_SHA1_RC4_40
+    algorithmType (PBE_SHA1_DES_EDE3_CBC _) = Type_PBE_SHA1_DES_EDE3_CBC
+    algorithmType (PBE_SHA1_DES_EDE2_CBC _) = Type_PBE_SHA1_DES_EDE2_CBC
+    algorithmType (PBE_SHA1_RC2_128 _)      = Type_PBE_SHA1_RC2_128
+    algorithmType (PBE_SHA1_RC2_40 _)       = Type_PBE_SHA1_RC2_40
+
+    parameterASN1S (PBES2 p)                 = asn1s p
+    parameterASN1S (PBE_MD5_DES_CBC p)       = asn1s p
+    parameterASN1S (PBE_SHA1_DES_CBC p)      = asn1s p
+    parameterASN1S (PBE_SHA1_RC4_128 p)      = asn1s p
+    parameterASN1S (PBE_SHA1_RC4_40 p)       = asn1s p
+    parameterASN1S (PBE_SHA1_DES_EDE3_CBC p) = asn1s p
+    parameterASN1S (PBE_SHA1_DES_EDE2_CBC p) = asn1s p
+    parameterASN1S (PBE_SHA1_RC2_128 p)      = asn1s p
+    parameterASN1S (PBE_SHA1_RC2_40 p)       = asn1s p
+
+    parseParameter Type_PBES2                 = PBES2 <$> parse
+    parseParameter Type_PBE_MD5_DES_CBC       = PBE_MD5_DES_CBC <$> parse
+    parseParameter Type_PBE_SHA1_DES_CBC      = PBE_SHA1_DES_CBC <$> parse
+    parseParameter Type_PBE_SHA1_RC4_128      = PBE_SHA1_RC4_128 <$> parse
+    parseParameter Type_PBE_SHA1_RC4_40       = PBE_SHA1_RC4_40 <$> parse
+    parseParameter Type_PBE_SHA1_DES_EDE3_CBC = PBE_SHA1_DES_EDE3_CBC <$> parse
+    parseParameter Type_PBE_SHA1_DES_EDE2_CBC = PBE_SHA1_DES_EDE2_CBC <$> parse
+    parseParameter Type_PBE_SHA1_RC2_128      = PBE_SHA1_RC2_128 <$> parse
+    parseParameter Type_PBE_SHA1_RC2_40       = PBE_SHA1_RC2_40 <$> parse
+
+instance ASN1Elem e => ProduceASN1Object e EncryptionScheme where
+    asn1s = algorithmASN1S Sequence
+
+instance Monoid e => ParseASN1Object e EncryptionScheme where
+    parse = parseAlgorithm Sequence
+
+
+-- High-level API
+
+-- | Content encrypted with a Password-Based Encryption Scheme (PBES).
+--
+-- The content will usually be the binary representation of an ASN.1 object,
+-- however the transformation may be applied to any bytestring.
+data PKCS5 = PKCS5
+    { encryptionAlgorithm :: EncryptionScheme -- ^ Scheme used to encrypt content
+    , encryptedData       :: EncryptedContent -- ^ Encrypted content
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e PKCS5 where
+    asn1s PKCS5{..} = asn1Container Sequence (alg . bs)
+      where alg = asn1s encryptionAlgorithm
+            bs  = gOctetString encryptedData
+
+instance Monoid e => ParseASN1Object e PKCS5 where
+    parse = onNextContainer Sequence $ do
+        alg <- parse
+        OctetString bs <- getNext
+        return PKCS5 { encryptionAlgorithm = alg, encryptedData = bs }
+
+instance ASN1Object PKCS5 where
+    toASN1   = asn1s
+    fromASN1 = runParseASN1State parse
+
+-- | Encrypt a bytestring with the specified encryption scheme and password.
+encrypt :: EncryptionScheme -> Password -> ByteString -> Either StoreError PKCS5
+encrypt alg pwd bs = build <$> pbEncrypt alg bs pwd
+  where
+    build ed = ed `seq` PKCS5 { encryptionAlgorithm = alg, encryptedData = ed }
+
+-- | Decrypt the PKCS #5 content with the specified password.
+decrypt :: PKCS5 -> Password -> Either StoreError ByteString
+decrypt obj = pbDecrypt (encryptionAlgorithm obj) (encryptedData obj)
+
+
+-- Encryption Schemes
+
+-- | Encrypt a bytestring with the specified encryption scheme and password.
+pbEncrypt :: EncryptionScheme -> ByteString -> Password
+          -> Either StoreError EncryptedContent
+pbEncrypt (PBES2 p)                 = pbes2  contentEncrypt p
+pbEncrypt (PBE_MD5_DES_CBC p)       = pkcs5  Left contentEncrypt MD5  DES p
+pbEncrypt (PBE_SHA1_DES_CBC p)      = pkcs5  Left contentEncrypt SHA1 DES p
+pbEncrypt (PBE_SHA1_RC4_128 p)      = pkcs12stream Left rc4Combine SHA1 16 p
+pbEncrypt (PBE_SHA1_RC4_40 p)       = pkcs12stream Left rc4Combine SHA1 5 p
+pbEncrypt (PBE_SHA1_DES_EDE3_CBC p) = pkcs12 Left contentEncrypt SHA1 DES_EDE3 p
+pbEncrypt (PBE_SHA1_DES_EDE2_CBC p) = pkcs12 Left contentEncrypt SHA1 DES_EDE2 p
+pbEncrypt (PBE_SHA1_RC2_128 p)      = pkcs12rc2 Left contentEncrypt SHA1 128 p
+pbEncrypt (PBE_SHA1_RC2_40 p)       = pkcs12rc2 Left contentEncrypt SHA1 40 p
+
+-- | Decrypt an encrypted bytestring with the specified encryption scheme and
+-- password.
+pbDecrypt :: EncryptionScheme -> EncryptedContent -> Password -> Either StoreError ByteString
+pbDecrypt (PBES2 p)                 = pbes2  contentDecrypt p
+pbDecrypt (PBE_MD5_DES_CBC p)       = pkcs5  Left contentDecrypt MD5  DES p
+pbDecrypt (PBE_SHA1_DES_CBC p)      = pkcs5  Left contentDecrypt SHA1 DES p
+pbDecrypt (PBE_SHA1_RC4_128 p)      = pkcs12stream Left rc4Combine SHA1 16 p
+pbDecrypt (PBE_SHA1_RC4_40 p)       = pkcs12stream Left rc4Combine SHA1 5 p
+pbDecrypt (PBE_SHA1_DES_EDE3_CBC p) = pkcs12 Left contentDecrypt SHA1 DES_EDE3 p
+pbDecrypt (PBE_SHA1_DES_EDE2_CBC p) = pkcs12 Left contentDecrypt SHA1 DES_EDE2 p
+pbDecrypt (PBE_SHA1_RC2_128 p)      = pkcs12rc2 Left contentDecrypt SHA1 128 p
+pbDecrypt (PBE_SHA1_RC2_40 p)       = pkcs12rc2 Left contentDecrypt SHA1 40 p
+
+pbes2 :: ByteArrayAccess password
+      => (Key -> ContentEncryptionParams -> ByteString -> result)
+      -> PBES2Parameter -> ByteString -> password -> result
+pbes2 encdec PBES2Parameter{..} bs pwd = encdec key pbes2EScheme bs
+  where key = kdfDerive pbes2KDF len pwd :: Key
+        len = fromMaybe (getMaximumKeySize pbes2EScheme) (kdfKeyLength pbes2KDF)
diff --git a/src/Crypto/Store/PKCS5/PBES1.hs b/src/Crypto/Store/PKCS5/PBES1.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PKCS5/PBES1.hs
@@ -0,0 +1,352 @@
+-- |
+-- Module      : Data.Store.PKCS5.PBES1
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Password-Based Encryption Schemes
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+module Crypto.Store.PKCS5.PBES1
+    ( PBEParameter(..)
+    , Key
+    , pkcs5
+    , pkcs12
+    , pkcs12rc2
+    , pkcs12stream
+    , pkcs12mac
+    , rc4Combine
+    ) where
+
+import           Basement.Block (Block)
+import           Basement.Compat.IsList
+import           Basement.Endianness
+import qualified Basement.String as S
+
+import           Crypto.Cipher.Types
+import qualified Crypto.Cipher.RC4 as RC4
+import qualified Crypto.Hash as Hash
+
+import           Data.ASN1.Types
+import           Data.Bits
+import           Data.ByteArray (ByteArray, ByteArrayAccess)
+import qualified Data.ByteArray as B
+import           Data.ByteString (ByteString)
+import           Data.Maybe (fromMaybe)
+import           Data.Memory.PtrMethods
+import           Data.Word
+
+import           Foreign.Ptr (plusPtr)
+import           Foreign.Storable
+
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.CMS.Algorithms
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+
+-- | Secret key.
+type Key = B.ScrubbedBytes
+
+-- | PBES1 parameters.
+data PBEParameter = PBEParameter
+    { pbeSalt           :: Salt -- ^ 8-octet salt value
+    , pbeIterationCount :: Int  -- ^ Iteration count
+    }
+    deriving (Show,Eq)
+
+instance ASN1Elem e => ProduceASN1Object e PBEParameter where
+    asn1s PBEParameter{..} =
+        let salt  = gOctetString pbeSalt
+            iters = gIntVal (toInteger pbeIterationCount)
+         in asn1Container Sequence (salt . iters)
+
+instance Monoid e => ParseASN1Object e PBEParameter where
+    parse = onNextContainer Sequence $ do
+        OctetString salt <- getNext
+        IntVal iters <- getNext
+        return PBEParameter { pbeSalt = salt
+                            , pbeIterationCount = fromInteger iters }
+
+cbcWith :: (BlockCipher cipher, ByteArrayAccess iv)
+        => ContentEncryptionCipher cipher -> iv -> ContentEncryptionParams
+cbcWith cipher iv = ParamsCBC cipher getIV
+  where
+    getIV = fromMaybe (error "PKCS5: bad initialization vector") (makeIV iv)
+
+rc2cbcWith :: ByteArrayAccess iv => Int -> iv -> ContentEncryptionParams
+rc2cbcWith len iv = ParamsCBCRC2 len getIV
+  where
+    getIV = fromMaybe (error "PKCS5: bad RC2 initialization vector") (makeIV iv)
+
+-- | RC4 encryption or decryption.
+rc4Combine :: (ByteArrayAccess key, ByteArray ba) => key -> ba -> Either StoreError ba
+rc4Combine key = Right . snd . RC4.combine (RC4.initialize key)
+
+-- | Conversion to UCS2 from UTF-8, ignoring non-BMP bits.
+toUCS2 :: (ByteArrayAccess butf8, ByteArray bucs2) => butf8 -> Maybe bucs2
+toUCS2 pwdUTF8
+    | B.null r  = Just pwdUCS2
+    | otherwise = Nothing
+  where
+    (p, _, r) = S.fromBytes S.UTF8 $ B.snoc (B.convert pwdUTF8) 0
+    pwdBlock  = fromList $ map ucs2 $ toList p :: Block (BE Word16)
+    pwdUCS2   = B.convert pwdBlock
+
+    ucs2 :: Char -> BE Word16
+    ucs2 = toBE . toEnum . fromEnum
+
+
+-- PBES1, RFC 8018 section 6.1.2
+
+-- | Apply PBKDF1 on the specified password and run an encryption or decryption
+-- function on some input using derived key and IV.
+pkcs5 :: (Hash.HashAlgorithm hash, BlockCipher cipher, ByteArrayAccess password)
+      => (StoreError -> result)
+      -> (Key -> ContentEncryptionParams -> ByteString -> result)
+      -> DigestProxy hash
+      -> ContentEncryptionCipher cipher
+      -> PBEParameter
+      -> ByteString
+      -> password
+      -> result
+pkcs5 failure encdec hashAlg cec pbeParam bs pwd
+    | proxyBlockSize cec /= 8 = failure (InvalidParameter "Invalid cipher block size")
+    | otherwise =
+        case pbkdf1 hashAlg pwd pbeParam 16 of
+            Left err -> failure err
+            Right dk ->
+                let (key, iv) = B.splitAt 8 (dk :: Key)
+                 in encdec key (cbcWith cec iv) bs
+
+
+-- PBKDF1, RFC 8018 section 5.1
+
+pbkdf1 :: (Hash.HashAlgorithm hash, ByteArrayAccess password, ByteArray out)
+       => DigestProxy hash
+       -> password
+       -> PBEParameter
+       -> Int
+       -> Either StoreError out
+pbkdf1 hashAlg pwd PBEParameter{..} dkLen
+    | dkLen > B.length t1 = Left (InvalidParameter "Derived key too long")
+    | otherwise           = Right (B.convert $ B.takeView tc dkLen)
+  where
+    a  = hashFromProxy hashAlg
+    t1 = Hash.hashFinalize (Hash.hashUpdate (Hash.hashUpdate (Hash.hashInitWith a) pwd) pbeSalt)
+    tc = iterate (Hash.hashWith a) t1 !! pred pbeIterationCount
+
+
+-- PKCS#12 encryption, RFC 7292 appendix B.2
+
+-- | Apply PKCS #12 derivation on the specified password and run an encryption
+-- or decryption function on some input using derived key and IV.
+pkcs12 :: (Hash.HashAlgorithm hash, BlockCipher cipher, ByteArrayAccess password)
+       => (StoreError -> result)
+       -> (Key -> ContentEncryptionParams -> ByteString -> result)
+       -> DigestProxy hash
+       -> ContentEncryptionCipher cipher
+       -> PBEParameter
+       -> ByteString
+       -> password
+       -> result
+pkcs12 failure encdec hashAlg cec pbeParam bs pwdUTF8 =
+    case toUCS2 pwdUTF8 of
+        Nothing      -> failure passwordNotUTF8
+        Just pwdUCS2 ->
+            let ivLen   = proxyBlockSize cec
+                iv      = pkcs12Derive hashAlg pbeParam 2 pwdUCS2 ivLen :: B.Bytes
+                eScheme = cbcWith cec iv
+                keyLen  = getMaximumKeySize eScheme
+                key     = pkcs12Derive hashAlg pbeParam 1 pwdUCS2 keyLen :: Key
+            in encdec key eScheme bs
+
+-- | Apply PKCS #12 derivation on the specified password and run an encryption
+-- or decryption function on some input using derived key and IV.  This variant
+-- uses an RC2 cipher with the EKL specified (effective key length).
+pkcs12rc2 :: (Hash.HashAlgorithm hash, ByteArrayAccess password)
+          => (StoreError -> result)
+          -> (Key -> ContentEncryptionParams -> ByteString -> result)
+          -> DigestProxy hash
+          -> Int
+          -> PBEParameter
+          -> ByteString
+          -> password
+          -> result
+pkcs12rc2 failure encdec hashAlg len pbeParam bs pwdUTF8 =
+    case toUCS2 pwdUTF8 of
+        Nothing      -> failure passwordNotUTF8
+        Just pwdUCS2 ->
+            let ivLen   = 8
+                iv      = pkcs12Derive hashAlg pbeParam 2 pwdUCS2 ivLen :: B.Bytes
+                eScheme = rc2cbcWith len iv
+                keyLen  = getMaximumKeySize eScheme
+                key     = pkcs12Derive hashAlg pbeParam 1 pwdUCS2 keyLen :: Key
+            in encdec key eScheme bs
+
+-- | Apply PKCS #12 derivation on the specified password and run an encryption
+-- or decryption function on some input using derived key.  This variant does
+-- not derive any IV and is required for RC4.
+pkcs12stream :: (Hash.HashAlgorithm hash, ByteArrayAccess password)
+             => (StoreError -> result)
+             -> (Key -> ByteString -> result)
+             -> DigestProxy hash
+             -> Int
+             -> PBEParameter
+             -> ByteString
+             -> password
+             -> result
+pkcs12stream failure encdec hashAlg keyLen pbeParam bs pwdUTF8 =
+    case toUCS2 pwdUTF8 of
+        Nothing      -> failure passwordNotUTF8
+        Just pwdUCS2 ->
+            let key = pkcs12Derive hashAlg pbeParam 1 pwdUCS2 keyLen :: Key
+             in encdec key bs
+
+-- | Apply PKCS #12 derivation on the specified password and run a MAC function
+-- on some input using derived key.
+pkcs12mac :: (Hash.HashAlgorithm hash, ByteArrayAccess password)
+          => (StoreError -> result)
+          -> (Key -> MACAlgorithm -> ByteString -> result)
+          -> DigestProxy hash
+          -> PBEParameter
+          -> ByteString
+          -> password
+          -> result
+pkcs12mac failure macFn hashAlg pbeParam bs pwdUTF8 =
+    case toUCS2 pwdUTF8 of
+        Nothing      -> failure passwordNotUTF8
+        Just pwdUCS2 ->
+            let macAlg = HMAC hashAlg
+                keyLen = getMaximumKeySize macAlg
+                key    = pkcs12Derive hashAlg pbeParam 3 pwdUCS2 keyLen :: Key
+            in macFn key macAlg bs
+
+passwordNotUTF8 :: StoreError
+passwordNotUTF8 = InvalidPassword "Provided password is not valid UTF-8"
+
+pkcs12Derive :: (Hash.HashAlgorithm hash, ByteArray bout)
+             => DigestProxy hash
+             -> PBEParameter
+             -> Word8
+             -> ByteString -- password (UCS2)
+             -> Int
+             -> bout
+pkcs12Derive hashAlg PBEParameter{..} idByte pwdUCS2 n =
+    B.take n $ B.concat $ take c $ loop t (s `B.append` p)
+  where
+    a = hashFromProxy hashAlg
+    v = getV (DigestAlgorithm hashAlg)
+    u = Hash.hashDigestSize a
+
+    c = (n + u - 1) `div` u
+    d = B.replicate v idByte :: B.Bytes
+    t = Hash.hashUpdate (Hash.hashInitWith a) d
+
+    p = pwdUCS2 `extendedToMult` v
+    s = pbeSalt `extendedToMult` v
+
+    loop :: Hash.HashAlgorithm hash
+         => Hash.Context hash -> ByteString -> [Hash.Digest hash]
+    loop x i = let z  = Hash.hashFinalize (Hash.hashUpdate x i)
+                   ai = iterate Hash.hash z !! pred pbeIterationCount
+                   b  = ai `extendedTo` v
+                   j  = B.concat $ map (add1 b) (chunks v i)
+                in ai : loop x j
+
+getV :: DigestAlgorithm -> Int
+getV (DigestAlgorithm MD2)    = 64
+getV (DigestAlgorithm MD4)    = 64
+getV (DigestAlgorithm MD5)    = 64
+getV (DigestAlgorithm SHA1)   = 64
+getV (DigestAlgorithm SHA224) = 64
+getV (DigestAlgorithm SHA256) = 64
+getV (DigestAlgorithm SHA384) = 128
+getV (DigestAlgorithm SHA512) = 128
+
+hashFromProxy :: proxy a -> a
+hashFromProxy _ = undefined
+
+-- Split in chunks of size 'n'
+chunks :: ByteArray ba => Int -> ba -> [ba]
+chunks n bs
+    | len > n   = let (c, cs) = B.splitAt n bs in c : chunks n cs
+    | len > 0   = [bs]
+    | otherwise = []
+  where
+    len = B.length bs
+
+-- Concatenate copies of input 'bs' to create output of length 'n'
+-- bytes (the final copy may be truncated)
+extendedTo :: (ByteArrayAccess bin, ByteArray bout) => bin -> Int -> bout
+bs `extendedTo` n =
+    B.allocAndFreeze n $ \pout ->
+        B.withByteArray bs $ \pin -> do
+            mapM_ (\off -> memCopy (pout `plusPtr` off) pin len)
+                  (enumFromThenTo 0 len (n - 1))
+            memCopy (pout `plusPtr` (n - r)) pin r
+  where
+    len = B.length bs
+    r   = n `mod` len
+{-# NOINLINE extendedTo #-}
+
+-- Concatenate copies of input 'bs' to create output whose length is a
+-- multiple of 'n' bytes (the final copy may be truncated).  If input
+-- is the empty string, so is the output.
+extendedToMult :: ByteArray ba => ba -> Int -> ba
+bs `extendedToMult` n
+    | len > n   = bs `B.append` B.take (n - len `mod` n) bs
+    | len == n  = bs
+    | len > 0   = bs `extendedTo` n
+    | otherwise = B.empty
+  where
+    len = B.length bs
+
+-- Add two bytearrays (considered as big-endian integers) and increment the
+-- result.  Output has size of the first bytearray.
+add1 :: ByteString -> ByteString -> ByteString
+add1 a b =
+    B.allocAndFreeze alen $ \pc ->
+        B.withByteArray a $ \pa ->
+        B.withByteArray b $ \pb ->
+            loop3 pa pb pc alen blen 1
+  where
+    alen = B.length a
+    blen = B.length b
+
+    -- main loop when both 'a' and 'b' have remaining bytes
+    loop3 !pa !pb !pc !ma !mb !c
+        | ma == 0   = return ()
+        | mb == 0   = loop2 pa pc ma c
+        | otherwise = do
+            let na = pred ma
+                nb = pred mb
+            ba <- peekElemOff pa na
+            bb <- peekElemOff pb nb
+            let (cc, bc) = carryAdd3 c ba bb
+            pokeElemOff pc na bc
+            loop3 pa pb pc na nb cc
+
+    -- when 'b' is smaller and bytes are exhausted we propagate
+    -- carry on 'a' alone
+    loop2 !pa !pc !ma !c
+        | ma == 0   = return ()
+        | otherwise = do
+            let na = pred ma
+            ba <- peekElemOff pa na
+            let (cc, bc) = carryAdd2 c ba
+            pokeElemOff pc na bc
+            loop2 pa pc na cc
+
+split16 :: Word16 -> (Word8, Word8)
+split16 x = (fromIntegral (shiftR x 8), fromIntegral x)
+
+carryAdd2 :: Word8 -> Word8 -> (Word8, Word8)
+carryAdd2 a b = split16 (fromIntegral a + fromIntegral b)
+
+carryAdd3 :: Word8 -> Word8 -> Word8 -> (Word8, Word8)
+carryAdd3 a b c = split16 (fromIntegral a + fromIntegral b + fromIntegral c)
diff --git a/src/Crypto/Store/PKCS8.hs b/src/Crypto/Store/PKCS8.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PKCS8.hs
@@ -0,0 +1,600 @@
+-- |
+-- Module      : Crypto.Store.PKCS8
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Private-Key Information Syntax, aka PKCS #8.
+--
+-- Presents an API similar to "Data.X509.Memory" and "Data.X509.File" but
+-- allows to write private keys and provides support for password-based
+-- encryption.
+--
+-- Functions to read a private key return an object wrapped in the
+-- 'OptProtected' data type.
+--
+-- Functions related to public keys, certificates and CRLs are available from
+-- "Crypto.Store.X509".
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE UndecidableInstances #-}
+module Crypto.Store.PKCS8
+    ( readKeyFile
+    , readKeyFileFromMemory
+    , pemToKey
+    , writeKeyFile
+    , writeKeyFileToMemory
+    , keyToPEM
+    , writeEncryptedKeyFile
+    , writeEncryptedKeyFileToMemory
+    , encryptKeyToPEM
+    -- * Serialization formats
+    , PrivateKeyFormat(..)
+    , FormattedKey(..)
+    -- * Password-based protection
+    , Password
+    , OptProtected(..)
+    , recover
+    , recoverA
+    -- * Reading and writing PEM files
+    , readPEMs
+    , writePEMs
+    ) where
+
+import Control.Applicative
+import Control.Monad (void, when)
+
+import Data.ASN1.Types
+import Data.ASN1.BinaryEncoding
+import Data.ASN1.BitArray
+import Data.ASN1.Encoding
+import Data.Maybe
+import qualified Data.X509 as X509
+import qualified Data.ByteString as B
+import           Crypto.Number.Serialize (i2osp, i2ospOf_, os2ip)
+import qualified Crypto.PubKey.DSA as DSA
+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
+import qualified Crypto.PubKey.RSA as RSA
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Attribute
+import Crypto.Store.CMS.Util
+import Crypto.Store.Error
+import Crypto.Store.PEM
+import Crypto.Store.PKCS5
+import Crypto.Store.PKCS8.EC
+import Crypto.Store.Util
+
+-- | Data type for objects that are possibly protected with a password.
+data OptProtected a = Unprotected a
+                      -- ^ Value is unprotected
+                    | Protected (Password -> Either StoreError a)
+                      -- ^ Value is protected with a password
+
+instance Functor OptProtected where
+    fmap f (Unprotected x) = Unprotected (f x)
+    fmap f (Protected g)   = Protected (fmap f . g)
+
+-- | Try to recover an 'OptProtected' content using the specified password.
+recover :: Password -> OptProtected a -> Either StoreError a
+recover _   (Unprotected x) = Right x
+recover pwd (Protected f)   = f pwd
+
+-- | Try to recover an 'OptProtected' content in an applicative context.  The
+-- applicative password is used if necessary.
+--
+-- > import qualified Data.ByteString as B
+-- > import           Crypto.Store.PKCS8
+-- >
+-- > [encryptedKey] <- readKeyFile "privkey.pem"
+-- > let askForPassword = putStr "Please enter password: " >> B.getLine
+-- > result <- recoverA askForPassword encryptedKey
+-- > case result of
+-- >     Left err  -> putStrLn $ "Unable to recover key: " ++ show err
+-- >     Right key -> print key
+recoverA :: Applicative f
+         => f Password -> OptProtected a -> f (Either StoreError a)
+recoverA _   (Unprotected x) = pure (Right x)
+recoverA get (Protected f)   = fmap f get
+
+
+-- Reading from PEM format
+
+-- | Read private keys from a PEM file.
+readKeyFile :: FilePath -> IO [OptProtected X509.PrivKey]
+readKeyFile path = accumulate <$> readPEMs path
+
+-- | Read private keys from a bytearray in PEM format.
+readKeyFileFromMemory :: B.ByteString -> [OptProtected X509.PrivKey]
+readKeyFileFromMemory = either (const []) accumulate . pemParseBS
+
+accumulate :: [PEM] -> [OptProtected X509.PrivKey]
+accumulate = catMaybes . foldr (flip pemToKey) []
+
+-- | Read a private key from a 'PEM' element and add it to the accumulator list.
+pemToKey :: [Maybe (OptProtected X509.PrivKey)] -> PEM -> [Maybe (OptProtected X509.PrivKey)]
+pemToKey acc pem =
+    case decodeASN1' BER (pemContent pem) of
+        Left _     -> acc
+        Right asn1 -> run (getParser $ pemName pem) asn1 : acc
+
+  where
+    run p = either (const Nothing) Just . runParseASN1 p
+
+    allTypes  = unFormat <$> parse
+    rsa       = X509.PrivKeyRSA . unFormat <$> parse
+    dsa       = X509.PrivKeyDSA . DSA.toPrivateKey . unFormat <$> parse
+    ecdsa     = X509.PrivKeyEC . unFormat <$> parse
+    encrypted = inner . decrypt <$> parse
+
+    getParser "PRIVATE KEY"           = Unprotected <$> allTypes
+    getParser "RSA PRIVATE KEY"       = Unprotected <$> rsa
+    getParser "DSA PRIVATE KEY"       = Unprotected <$> dsa
+    getParser "EC PRIVATE KEY"        = Unprotected <$> ecdsa
+    getParser "ENCRYPTED PRIVATE KEY" = Protected   <$> encrypted
+    getParser _                       = empty
+
+    inner decfn pwd = do
+        decrypted <- decfn pwd
+        asn1 <- mapLeft DecodingError $ decodeASN1' BER decrypted
+        case run allTypes asn1 of
+            Nothing -> Left (ParseFailure "No key parsed after decryption")
+            Just k  -> return k
+
+
+-- Writing to PEM format
+
+-- | Write unencrypted private keys to a PEM file.
+writeKeyFile :: PrivateKeyFormat -> FilePath -> [X509.PrivKey] -> IO ()
+writeKeyFile fmt path = writePEMs path . map (keyToPEM fmt)
+
+-- | Write unencrypted private keys to a bytearray in PEM format.
+writeKeyFileToMemory :: PrivateKeyFormat -> [X509.PrivKey] -> B.ByteString
+writeKeyFileToMemory fmt = pemsWriteBS . map (keyToPEM fmt)
+
+-- | Write a PKCS #8 encrypted private key to a PEM file.
+--
+-- If multiple keys need to be stored in the same file, use functions
+-- 'encryptKeyToPEM' and 'writePEMs'.
+--
+-- Fresh 'EncryptionScheme' parameters should be generated for each key to
+-- encrypt.
+writeEncryptedKeyFile :: FilePath
+                      -> EncryptionScheme -> Password-> X509.PrivKey
+                      -> IO (Either StoreError ())
+writeEncryptedKeyFile path alg pwd privKey =
+    let pem = encryptKeyToPEM alg pwd privKey
+     in either (return . Left) (fmap Right . writePEMs path . (:[])) pem
+
+-- | Write a PKCS #8 encrypted private key to a bytearray in PEM format.
+--
+-- If multiple keys need to be stored in the same bytearray, use functions
+-- 'encryptKeyToPEM' and 'pemWriteBS' or 'pemWriteLBS'.
+--
+-- Fresh 'EncryptionScheme' parameters should be generated for each key to
+-- encrypt.
+writeEncryptedKeyFileToMemory :: EncryptionScheme -> Password -> X509.PrivKey
+                              -> Either StoreError B.ByteString
+writeEncryptedKeyFileToMemory alg pwd privKey =
+    pemWriteBS <$> encryptKeyToPEM alg pwd privKey
+
+-- | Generate an unencrypted PEM for a private key.
+keyToPEM :: PrivateKeyFormat -> X509.PrivKey -> PEM
+keyToPEM TraditionalFormat = keyToTraditionalPEM
+keyToPEM PKCS8Format       = keyToModernPEM
+
+keyToTraditionalPEM :: X509.PrivKey -> PEM
+keyToTraditionalPEM privKey =
+    mkPEM (typeTag ++ " PRIVATE KEY") (encodeASN1S asn1)
+  where (typeTag, asn1) = traditionalPrivKeyASN1S privKey
+
+traditionalPrivKeyASN1S :: ASN1Elem e => X509.PrivKey -> (String, ASN1Stream e)
+traditionalPrivKeyASN1S privKey =
+    case privKey of
+        X509.PrivKeyRSA k -> ("RSA", traditional k)
+        X509.PrivKeyDSA k -> ("DSA", traditional (dsaPrivToPair k))
+        X509.PrivKeyEC  k -> ("EC",  traditional k)
+  where
+    traditional a = asn1s (Traditional a)
+
+keyToModernPEM :: X509.PrivKey -> PEM
+keyToModernPEM privKey = mkPEM "PRIVATE KEY" (encodeASN1S asn1)
+  where asn1 = modernPrivKeyASN1S [] privKey
+
+modernPrivKeyASN1S :: ASN1Elem e => [Attribute] -> X509.PrivKey -> ASN1Stream e
+modernPrivKeyASN1S attrs privKey =
+    case privKey of
+        X509.PrivKeyRSA k -> modern k
+        X509.PrivKeyDSA k -> modern (dsaPrivToPair k)
+        X509.PrivKeyEC  k -> modern k
+  where
+    modern a = asn1s (Modern attrs a)
+
+-- | Generate a PKCS #8 encrypted PEM for a private key.
+--
+-- Fresh 'EncryptionScheme' parameters should be generated for each key to
+-- encrypt.
+encryptKeyToPEM :: EncryptionScheme -> Password -> X509.PrivKey
+                -> Either StoreError PEM
+encryptKeyToPEM alg pwd privKey = toPEM <$> encrypt alg pwd bs
+  where bs = pemContent (keyToModernPEM privKey)
+        toPEM pkcs8 = mkPEM "ENCRYPTED PRIVATE KEY" (encodeASN1Object pkcs8)
+
+mkPEM :: String -> B.ByteString -> PEM
+mkPEM name bs = PEM { pemName = name, pemHeader = [], pemContent = bs}
+
+
+-- Private key formats: traditional (SSLeay compatible) and modern (PKCS #8)
+
+-- | Private-key serialization format.
+--
+-- Encryption in traditional format is not supported currently.
+data PrivateKeyFormat = TraditionalFormat -- ^ SSLeay compatible
+                      | PKCS8Format       -- ^ PKCS #8
+                      deriving (Show,Eq)
+
+newtype Traditional a = Traditional { unTraditional :: a }
+
+parseTraditional :: ParseASN1Object e (Traditional a) => ParseASN1 e a
+parseTraditional = unTraditional <$> parse
+
+data Modern a = Modern [Attribute] a
+
+instance Functor Modern where
+    fmap f (Modern attrs a) = Modern attrs (f a)
+
+parseModern :: ParseASN1Object e (Modern a) => ParseASN1 e a
+parseModern = unModern <$> parse
+  where unModern (Modern _ a) = a
+
+-- | A key associated with format.  Allows to implement 'ASN1Object' instances.
+data FormattedKey a = FormattedKey PrivateKeyFormat a
+    deriving (Show,Eq)
+
+instance Functor FormattedKey where
+    fmap f (FormattedKey fmt a) = FormattedKey fmt (f a)
+
+instance (ProduceASN1Object e (Traditional a), ProduceASN1Object e (Modern a)) => ProduceASN1Object e (FormattedKey a) where
+    asn1s (FormattedKey TraditionalFormat k) = asn1s (Traditional k)
+    asn1s (FormattedKey PKCS8Format k)       = asn1s (Modern [] k)
+
+instance (Monoid e, ParseASN1Object e (Traditional a), ParseASN1Object e (Modern a)) => ParseASN1Object e (FormattedKey a) where
+    parse = (traditional <$> parseTraditional) <|> (modern <$> parseModern)
+      where
+        traditional = FormattedKey TraditionalFormat
+        modern      = FormattedKey PKCS8Format
+
+unFormat :: FormattedKey a -> a
+unFormat (FormattedKey _ a) = a
+
+
+-- Private Keys
+
+instance ASN1Object (FormattedKey X509.PrivKey) where
+    toASN1   = asn1s
+    fromASN1 = runParseASN1State parse
+
+instance ASN1Elem e => ProduceASN1Object e (Traditional X509.PrivKey) where
+    asn1s (Traditional privKey) = snd $ traditionalPrivKeyASN1S privKey
+
+instance Monoid e => ParseASN1Object e (Traditional X509.PrivKey) where
+    parse = rsa <|> dsa <|> ecdsa
+      where
+        rsa   = Traditional . X509.PrivKeyRSA . unTraditional <$> parse
+        dsa   = Traditional . X509.PrivKeyDSA . DSA.toPrivateKey . unTraditional <$> parse
+        ecdsa = Traditional . X509.PrivKeyEC . unTraditional <$> parse
+
+instance ASN1Elem e => ProduceASN1Object e (Modern X509.PrivKey) where
+    asn1s (Modern attrs privKey) = modernPrivKeyASN1S attrs privKey
+
+instance Monoid e => ParseASN1Object e (Modern X509.PrivKey) where
+    parse = rsa <|> dsa <|> ecdsa
+      where
+        rsa   = fmap X509.PrivKeyRSA  <$> parse
+        dsa   = fmap (X509.PrivKeyDSA . DSA.toPrivateKey) <$> parse
+        ecdsa = fmap X509.PrivKeyEC <$> parse
+
+skipVersion :: Monoid e => ParseASN1 e ()
+skipVersion = do
+    IntVal v <- getNext
+    when (v /= 0 && v /= 1) $
+        throwParseError ("PKCS8: parsed invalid version: " ++ show v)
+
+skipPublicKey :: Monoid e => ParseASN1 e ()
+skipPublicKey = void (fmap Just parseTaggedPrimitive <|> return Nothing)
+  where parseTaggedPrimitive = do { Other _ 1 bs <- getNext; return bs }
+
+parseAttrKeys :: Monoid e => ParseASN1 e ([Attribute], B.ByteString)
+parseAttrKeys = do
+    OctetString bs <- getNext
+    attrs <- parseAttributes (Container Context 0)
+    skipPublicKey
+    return (attrs, bs)
+
+
+-- RSA
+
+instance ASN1Object (FormattedKey RSA.PrivateKey) where
+    toASN1   = asn1s
+    fromASN1 = runParseASN1State parse
+
+instance ASN1Elem e => ProduceASN1Object e (Traditional RSA.PrivateKey) where
+    asn1s (Traditional privKey) =
+        asn1Container Sequence (v . n . e . d . p1 . p2 . pexp1 . pexp2 . pcoef)
+      where
+        pubKey = RSA.private_pub privKey
+
+        v     = gIntVal 0
+        n     = gIntVal (RSA.public_n pubKey)
+        e     = gIntVal (RSA.public_e pubKey)
+        d     = gIntVal (RSA.private_d privKey)
+        p1    = gIntVal (RSA.private_p privKey)
+        p2    = gIntVal (RSA.private_q privKey)
+        pexp1 = gIntVal (RSA.private_dP privKey)
+        pexp2 = gIntVal (RSA.private_dQ privKey)
+        pcoef = gIntVal (RSA.private_qinv privKey)
+
+instance Monoid e => ParseASN1Object e (Traditional RSA.PrivateKey) where
+    parse = onNextContainer Sequence $ do
+        IntVal 0 <- getNext
+        IntVal n <- getNext
+        IntVal e <- getNext
+        IntVal d <- getNext
+        IntVal p1 <- getNext
+        IntVal p2 <- getNext
+        IntVal pexp1 <- getNext
+        IntVal pexp2 <- getNext
+        IntVal pcoef <- getNext
+        let calculate_modulus m i =
+                if (2 ^ (i * 8)) > m
+                    then i
+                    else calculate_modulus m (i+1)
+            pubKey  = RSA.PublicKey { RSA.public_size = calculate_modulus n 1
+                                    , RSA.public_n    = n
+                                    , RSA.public_e    = e
+                                    }
+            privKey = RSA.PrivateKey { RSA.private_pub  = pubKey
+                                    , RSA.private_d    = d
+                                    , RSA.private_p    = p1
+                                    , RSA.private_q    = p2
+                                    , RSA.private_dP   = pexp1
+                                    , RSA.private_dQ   = pexp2
+                                    , RSA.private_qinv = pcoef
+                                    }
+        return (Traditional privKey)
+
+instance ASN1Elem e => ProduceASN1Object e (Modern RSA.PrivateKey) where
+    asn1s (Modern attrs privKey) =
+        asn1Container Sequence (v . alg . bs . att)
+      where
+        v     = gIntVal 0
+        alg   = asn1Container Sequence (oid . gNull)
+        oid   = gOID [1,2,840,113549,1,1,1]
+        bs    = gOctetString (encodeASN1Object $ Traditional privKey)
+        att   = attributesASN1S (Container Context 0) attrs
+
+instance Monoid e => ParseASN1Object e (Modern RSA.PrivateKey) where
+    parse = onNextContainer Sequence $ do
+        skipVersion
+        Null <- onNextContainer Sequence $ do
+                    OID [1,2,840,113549,1,1,1] <- getNext
+                    getNext
+        (attrs, bs) <- parseAttrKeys
+        let inner = decodeASN1' BER bs
+            strError = Left .  ("PKCS8: error decoding inner RSA: " ++) . show
+        case either strError (runParseASN1 parseTraditional) inner of
+             Left err -> throwParseError ("PKCS8: error parsing inner RSA: " ++ err)
+             Right privKey -> return (Modern attrs privKey)
+
+
+-- DSA
+
+instance ASN1Object (FormattedKey DSA.KeyPair) where
+    toASN1   = asn1s
+    fromASN1 = runParseASN1State parse
+
+instance ASN1Elem e => ProduceASN1Object e (Traditional DSA.KeyPair) where
+    asn1s (Traditional (DSA.KeyPair params pub priv)) =
+        asn1Container Sequence (v . pqgASN1S params . pub' . priv')
+      where
+        v     = gIntVal 0
+        pub'  = gIntVal pub
+        priv' = gIntVal priv
+
+instance Monoid e => ParseASN1Object e (Traditional DSA.KeyPair) where
+    parse = onNextContainer Sequence $ do
+        IntVal 0 <- getNext
+        params <- parsePQG
+        IntVal pub <- getNext
+        IntVal priv <- getNext
+        return (Traditional $ DSA.KeyPair params pub priv)
+
+instance ASN1Elem e => ProduceASN1Object e (Modern DSA.KeyPair) where
+    asn1s (Modern attrs (DSA.KeyPair params _ priv)) =
+        asn1Container Sequence (v . alg . bs . att)
+      where
+        v     = gIntVal 0
+        alg   = asn1Container Sequence (oid . pr)
+        oid   = gOID [1,2,840,10040,4,1]
+        pr    = asn1Container Sequence (pqgASN1S params)
+        bs    = gOctetString (encodeASN1S $ gIntVal priv)
+        att   = attributesASN1S (Container Context 0) attrs
+
+instance Monoid e => ParseASN1Object e (Modern DSA.KeyPair) where
+    parse = onNextContainer Sequence $ do
+        skipVersion
+        params <- onNextContainer Sequence $ do
+                      OID [1,2,840,10040,4,1] <- getNext
+                      onNextContainer Sequence parsePQG
+        (attrs, bs) <- parseAttrKeys
+        case decodeASN1' BER bs of
+            Right [IntVal priv] ->
+                let pub = DSA.calculatePublic params priv
+                 in return (Modern attrs $ DSA.KeyPair params pub priv)
+            Right _ -> throwParseError "PKCS8: invalid format when parsing inner DSA"
+            Left  e -> throwParseError ("PKCS8: error parsing inner DSA: " ++ show e)
+
+pqgASN1S :: ASN1Elem e => DSA.Params -> ASN1Stream e
+pqgASN1S params = p . q . g
+  where p = gIntVal (DSA.params_p params)
+        q = gIntVal (DSA.params_q params)
+        g = gIntVal (DSA.params_g params)
+
+parsePQG :: Monoid e => ParseASN1 e DSA.Params
+parsePQG = do
+    IntVal p <- getNext
+    IntVal q <- getNext
+    IntVal g <- getNext
+    return DSA.Params { DSA.params_p = p
+                      , DSA.params_q = q
+                      , DSA.params_g = g
+                      }
+
+dsaPrivToPair :: DSA.PrivateKey -> DSA.KeyPair
+dsaPrivToPair k = DSA.KeyPair params pub x
+  where pub     = DSA.calculatePublic params x
+        params  = DSA.private_params k
+        x       = DSA.private_x k
+
+
+-- ECDSA
+
+instance ASN1Object (FormattedKey X509.PrivKeyEC) where
+    toASN1   = asn1s
+    fromASN1 = runParseASN1State parse
+
+instance ASN1Elem e => ProduceASN1Object e (Traditional X509.PrivKeyEC) where
+    asn1s = innerEcdsaASN1S True . unTraditional
+
+instance Monoid e => ParseASN1Object e (Traditional X509.PrivKeyEC) where
+    parse = Traditional <$> parseInnerEcdsa Nothing
+
+instance ASN1Elem e => ProduceASN1Object e (Modern X509.PrivKeyEC) where
+    asn1s (Modern attrs privKey) = asn1Container Sequence (v . f . bs . att)
+      where
+        v     = gIntVal 0
+        f     = asn1Container Sequence (oid . curveFnASN1S privKey)
+        oid   = gOID [1,2,840,10045,2,1]
+        bs    = gOctetString (encodeASN1S inner)
+        inner = innerEcdsaASN1S False privKey
+        att   = attributesASN1S (Container Context 0) attrs
+
+instance Monoid e => ParseASN1Object e (Modern X509.PrivKeyEC) where
+    parse = onNextContainer Sequence $ do
+        skipVersion
+        f <- onNextContainer Sequence $ do
+            OID [1,2,840,10045,2,1] <- getNext
+            parseCurveFn
+        (attrs, bs) <- parseAttrKeys
+        let inner = decodeASN1' BER bs
+            strError = Left .  ("PKCS8: error decoding inner EC: " ++) . show
+        case either strError (runParseASN1 $ parseInnerEcdsa $ Just f) inner of
+            Left err -> throwParseError ("PKCS8: error parsing inner EC: " ++ err)
+            Right privKey -> return (Modern attrs privKey)
+
+innerEcdsaASN1S :: ASN1Elem e => Bool -> X509.PrivKeyEC -> ASN1Stream e
+innerEcdsaASN1S addC k
+    | addC      = asn1Container Sequence (v . ds . c0 . c1)
+    | otherwise = asn1Container Sequence (v . ds . c1)
+  where
+    curve = fromMaybe (error "PKCS8: invalid EC parameters") (ecPrivKeyCurve k)
+    bytes = curveOrderBytes curve
+
+    v  = gIntVal 1
+    ds = gOctetString (i2ospOf_ bytes (X509.privkeyEC_priv k))
+    c0 = asn1Container (Container Context 0) (curveFnASN1S k)
+    c1 = asn1Container (Container Context 1) pub
+
+    pub = gBitString (toBitArray sp 0)
+    X509.SerializedPoint sp = getSerializedPoint curve (X509.privkeyEC_priv k)
+
+parseInnerEcdsa :: Monoid e
+                => Maybe (ECDSA.PrivateNumber -> X509.PrivKeyEC)
+                -> ParseASN1 e X509.PrivKeyEC
+parseInnerEcdsa fn = onNextContainer Sequence $ do
+    IntVal 1 <- getNext
+    OctetString ds <- getNext
+    let d = os2ip ds
+    m <- onNextContainerMaybe (Container Context 0) parseCurveFn
+    _ <- onNextContainerMaybe (Container Context 1) parsePK
+    case fn <|> m of
+        Nothing     -> throwParseError "PKCS8: no curve found in EC private key"
+        Just getKey -> return (getKey d)
+  where
+    parsePK = do { BitString bs <- getNext; return bs }
+
+curveFnASN1S :: ASN1Elem e => X509.PrivKeyEC -> ASN1Stream e
+curveFnASN1S X509.PrivKeyEC_Named{..} = gOID (curveNameOID privkeyEC_name)
+curveFnASN1S X509.PrivKeyEC_Prime{..} =
+    asn1Container Sequence (v . prime . abSeed . gen . o . c)
+  where
+    X509.SerializedPoint generator = privkeyEC_generator
+    bytes  = numBytes privkeyEC_prime
+
+    v      = gIntVal 1
+
+    prime  = asn1Container Sequence (oid . p)
+    oid    = gOID [1,2,840,10045,1,1]
+    p      = gIntVal privkeyEC_prime
+
+    abSeed = asn1Container Sequence (a . b . seed)
+    a      = gOctetString (i2ospOf_ bytes privkeyEC_a)
+    b      = gOctetString (i2ospOf_ bytes privkeyEC_b)
+    seed   = if privkeyEC_seed > 0
+                 then gBitString (toBitArray (i2osp privkeyEC_seed) 0)
+                 else id
+
+    gen    = gOctetString generator
+    o      = gIntVal privkeyEC_order
+    c      = gIntVal privkeyEC_cofactor
+
+parseCurveFn :: Monoid e => ParseASN1 e (ECDSA.PrivateNumber -> X509.PrivKeyEC)
+parseCurveFn = parseNamedCurve <|> parsePrimeCurve
+  where
+    parseNamedCurve = do
+        OID oid <- getNext
+        case lookupCurveNameByOID oid of
+            Just name -> return $ \d ->
+                            X509.PrivKeyEC_Named
+                                { X509.privkeyEC_name = name
+                                , X509.privkeyEC_priv = d
+                                }
+            Nothing -> throwParseError ("PKCS8: unknown EC curve with OID " ++ show oid)
+
+    parsePrimeCurve =
+        onNextContainer Sequence $ do
+            IntVal 1 <- getNext
+            prime <- onNextContainer Sequence $ do
+                OID [1,2,840,10045,1,1] <- getNext
+                IntVal prime <- getNext
+                return prime
+            (a, b, seed) <- onNextContainer Sequence $ do
+                OctetString a <- getNext
+                OctetString b <- getNext
+                seed <- parseOptionalSeed
+                return (a, b, seed)
+            OctetString generator <- getNext
+            IntVal order <- getNext
+            IntVal cofactor <- getNext
+            return $ \d ->
+                X509.PrivKeyEC_Prime
+                    { X509.privkeyEC_priv      = d
+                    , X509.privkeyEC_a         = os2ip a
+                    , X509.privkeyEC_b         = os2ip b
+                    , X509.privkeyEC_prime     = prime
+                    , X509.privkeyEC_generator = X509.SerializedPoint generator
+                    , X509.privkeyEC_order     = order
+                    , X509.privkeyEC_cofactor  = cofactor
+                    , X509.privkeyEC_seed      = seed
+                    }
+
+    parseOptionalSeed = do
+        seedAvail <- hasNext
+        if seedAvail
+            then do BitString seed <- getNext
+                    return (os2ip $ bitArrayGetData seed)
+            else return 0
diff --git a/src/Crypto/Store/PKCS8/EC.hs b/src/Crypto/Store/PKCS8/EC.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/PKCS8/EC.hs
@@ -0,0 +1,99 @@
+-- |
+-- Module      : Crypto.Store.PKCS8.EC
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Additional EC utilities.
+module Crypto.Store.PKCS8.EC
+    ( numBytes
+    , curveSizeBytes
+    , curveOrderBytes
+    , curveNameOID
+    , getSerializedPoint
+    , module Data.X509.EC
+    ) where
+
+import           Data.ASN1.OID
+import qualified Data.ByteString as B
+import           Data.Maybe (fromMaybe)
+
+import Data.X509
+import Data.X509.EC
+
+import Crypto.Number.Basic (numBits)
+import Crypto.Number.Serialize (i2ospOf_)
+import Crypto.PubKey.ECC.Prim
+import Crypto.PubKey.ECC.Types
+
+import Crypto.Store.CMS.Util
+
+-- | Number of bytes necessary to serialize n bits.
+bitsToBytes :: Int -> Int
+bitsToBytes n = (n + 7) `div` 8
+
+-- | Number of bytes necessary to serialize an integer.
+numBytes :: Integer -> Int
+numBytes = bitsToBytes . numBits
+
+-- | Number of bytes to serialize a field element.
+curveSizeBytes :: Curve -> Int
+curveSizeBytes = bitsToBytes . curveSizeBits
+
+-- | Number of bytes to serialize a scalar.
+curveOrderBytes :: Curve -> Int
+curveOrderBytes = bitsToBytes . numBits . ecc_n . common_curve
+
+-- | Transform a private scalar to a point in uncompressed format.
+getSerializedPoint :: Curve -> PrivateNumber -> SerializedPoint
+getSerializedPoint curve d = SerializedPoint (serializePoint pt)
+  where
+    pt = pointBaseMul curve d
+    bs = i2ospOf_ (curveSizeBytes curve)
+
+    serializePoint PointO      = B.singleton 0
+    serializePoint (Point x y) = B.cons 4 (B.append (bs x) (bs y))
+
+-- | Return the OID associated to a curve name.
+curveNameOID :: CurveName -> OID
+curveNameOID name =
+    fromMaybe (error $ "PKCS8: OID unknown for EC curve " ++ show name)
+        (lookupOID curvesOIDTable name)
+
+curvesOIDTable :: OIDTable CurveName
+curvesOIDTable =
+    [ (SEC_p112r1, [1,3,132,0,6])
+    , (SEC_p112r2, [1,3,132,0,7])
+    , (SEC_p128r1, [1,3,132,0,28])
+    , (SEC_p128r2, [1,3,132,0,29])
+    , (SEC_p160k1, [1,3,132,0,9])
+    , (SEC_p160r1, [1,3,132,0,8])
+    , (SEC_p160r2, [1,3,132,0,30])
+    , (SEC_p192k1, [1,3,132,0,31])
+    , (SEC_p192r1, [1,2,840,10045,3,1,1])
+    , (SEC_p224k1, [1,3,132,0,32])
+    , (SEC_p224r1, [1,3,132,0,33])
+    , (SEC_p256k1, [1,3,132,0,10])
+    , (SEC_p256r1, [1,2,840,10045,3,1,7])
+    , (SEC_p384r1, [1,3,132,0,34])
+    , (SEC_p521r1, [1,3,132,0,35])
+    , (SEC_t113r1, [1,3,132,0,4])
+    , (SEC_t113r2, [1,3,132,0,5])
+    , (SEC_t131r1, [1,3,132,0,22])
+    , (SEC_t131r2, [1,3,132,0,23])
+    , (SEC_t163k1, [1,3,132,0,1])
+    , (SEC_t163r1, [1,3,132,0,2])
+    , (SEC_t163r2, [1,3,132,0,15])
+    , (SEC_t193r1, [1,3,132,0,24])
+    , (SEC_t193r2, [1,3,132,0,25])
+    , (SEC_t233k1, [1,3,132,0,26])
+    , (SEC_t233r1, [1,3,132,0,27])
+    , (SEC_t239k1, [1,3,132,0,3])
+    , (SEC_t283k1, [1,3,132,0,16])
+    , (SEC_t283r1, [1,3,132,0,17])
+    , (SEC_t409k1, [1,3,132,0,36])
+    , (SEC_t409r1, [1,3,132,0,37])
+    , (SEC_t571k1, [1,3,132,0,38])
+    , (SEC_t571r1, [1,3,132,0,39])
+    ]
diff --git a/src/Crypto/Store/Util.hs b/src/Crypto/Store/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/Util.hs
@@ -0,0 +1,45 @@
+-- |
+-- Module      : Crypto.Store.Util
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+--
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE MagicHash #-}
+module Crypto.Store.Util
+    ( (&&!)
+    , reverseBytes
+    , constAllEq
+    , mapLeft
+    ) where
+
+import           Data.Bits
+import           Data.ByteArray (ByteArray, ByteArrayAccess)
+import qualified Data.ByteArray as B
+import           Data.List
+import           Data.Word
+
+import GHC.Exts
+
+-- | This is a strict version of &&.
+(&&!) :: Bool -> Bool -> Bool
+(&&!) x y = isTrue# (andI# (getTag# x) (getTag# y))
+  where getTag# !z = dataToTag# z
+infixr 3 &&!
+
+-- | Reverse a bytearray.
+reverseBytes :: (ByteArrayAccess bin, ByteArray bout) => bin -> bout
+reverseBytes = B.pack . reverse . B.unpack
+
+-- | Test if all bytes in a bytearray are equal to the value specified.  Runs in
+-- constant time.
+constAllEq :: ByteArrayAccess ba => Word8 -> ba -> Bool
+constAllEq b = (== 0) . foldl' fn 0 . B.unpack
+  where fn acc x = acc .|. xor b x
+
+-- | Map over the left value.
+mapLeft :: (a -> b) -> Either a c -> Either b c
+mapLeft f (Left a)  = Left (f a)
+mapLeft _ (Right c) = Right c
diff --git a/src/Crypto/Store/X509.hs b/src/Crypto/Store/X509.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/Store/X509.hs
@@ -0,0 +1,178 @@
+-- |
+-- Module      : Crypto.Store.X509
+-- License     : BSD-style
+-- Maintainer  : Olivier Chéron <olivier.cheron@gmail.com>
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Public keys, certificates and CRLs.
+--
+-- Presents an API similar to "Data.X509.Memory" and "Data.X509.File" but
+-- provides support for public-key files and allows to write objects.
+--
+-- Functions related to private keys are available from "Crypto.Store.PKCS8".
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module Crypto.Store.X509
+    ( SignedObject()
+    -- * Public keys
+    , readPubKeyFile
+    , readPubKeyFileFromMemory
+    , pemToPubKey
+    , writePubKeyFile
+    , writePubKeyFileToMemory
+    , pubKeyToPEM
+    -- * Signed objects
+    , readSignedObject
+    , readSignedObjectFromMemory
+    , writeSignedObject
+    , writeSignedObjectToMemory
+    -- * Reading and writing PEM files
+    , readPEMs
+    , writePEMs
+    ) where
+
+import Data.ASN1.Types
+import Data.ASN1.BinaryEncoding
+import Data.ASN1.Encoding
+import Data.Maybe
+import Data.Proxy
+import qualified Data.X509 as X509
+import qualified Data.ByteString as B
+import qualified Crypto.PubKey.RSA as RSA
+
+import Crypto.Store.ASN1.Generate
+import Crypto.Store.ASN1.Parse
+import Crypto.Store.CMS.Util
+import Crypto.Store.PEM
+
+
+-- | Class of signed objects convertible to PEM.
+class (ASN1Object a, Eq a, Show a) => SignedObject a where
+    signedObjectName :: proxy a -> String
+    otherObjectNames :: proxy a -> [String]
+
+instance SignedObject X509.Certificate where
+    signedObjectName _ = "CERTIFICATE"
+    otherObjectNames _ = ["X509 CERTIFICATE"]
+
+instance SignedObject X509.CRL where
+    signedObjectName _ = "X509 CRL"
+    otherObjectNames _ = []
+
+validObjectName :: SignedObject a => proxy a -> String -> Bool
+validObjectName prx name =
+    name == signedObjectName prx || name `elem` otherObjectNames prx
+
+
+-- Reading from PEM format
+
+-- | Read public keys from a PEM file.
+readPubKeyFile :: FilePath -> IO [X509.PubKey]
+readPubKeyFile path = accumulate <$> readPEMs path
+
+-- | Read public keys from a bytearray in PEM format.
+readPubKeyFileFromMemory :: B.ByteString -> [X509.PubKey]
+readPubKeyFileFromMemory = either (const []) accumulate . pemParseBS
+
+accumulate :: [PEM] -> [X509.PubKey]
+accumulate = catMaybes . foldr (flip pemToPubKey) []
+
+-- | Read a public key from a 'PEM' element and add it to the accumulator list.
+pemToPubKey :: [Maybe X509.PubKey] -> PEM -> [Maybe X509.PubKey]
+pemToPubKey acc pem =
+    case decodeASN1' BER (pemContent pem) of
+        Left _     -> acc
+        Right asn1 -> run (getParser $ pemName pem) asn1 : acc
+
+  where
+    run p asn1 =
+        case p asn1 of
+            Right (pubKey, []) -> Just pubKey
+            _                  -> Nothing
+
+    getParser "PUBLIC KEY"           = fromASN1
+    getParser "RSA PUBLIC KEY"       = runParseASN1State rsapkParser
+    getParser _                      = const (Left undefined)
+
+    rsapkParser = (\(RSAPublicKey pub) -> X509.PubKeyRSA pub) <$> parse
+
+-- | Read signed objects from a PEM file (only one type at a time).
+readSignedObject :: SignedObject a => FilePath -> IO [X509.SignedExact a]
+readSignedObject path = accumulate' <$> readPEMs path
+
+-- | Read signed objects from a bytearray in PEM format (only one type at a
+-- time).
+readSignedObjectFromMemory :: SignedObject a
+                           => B.ByteString
+                           -> [X509.SignedExact a]
+readSignedObjectFromMemory = either (const []) accumulate' . pemParseBS
+
+accumulate' :: forall a. SignedObject a => [PEM] -> [X509.SignedExact a]
+accumulate' = foldr pemToSigned []
+  where
+    prx = Proxy :: Proxy a
+
+    pemToSigned pem acc
+        | validObjectName prx (pemName pem) =
+            case X509.decodeSignedObject $ pemContent pem of
+                Left _    -> acc
+                Right obj -> obj : acc
+        | otherwise = acc
+
+
+-- Writing to PEM format
+
+-- | Write public keys to a PEM file.
+writePubKeyFile :: FilePath -> [X509.PubKey] -> IO ()
+writePubKeyFile path = writePEMs path . map pubKeyToPEM
+
+-- | Write public keys to a bytearray in PEM format.
+writePubKeyFileToMemory :: [X509.PubKey] -> B.ByteString
+writePubKeyFileToMemory = pemsWriteBS . map pubKeyToPEM
+
+-- | Generate a PEM for a public key.
+pubKeyToPEM :: X509.PubKey -> PEM
+pubKeyToPEM pubKey = mkPEM "PUBLIC KEY" (encodeASN1S $ gMany asn1)
+  where asn1 = toASN1 pubKey []
+
+-- | Write signed objects to a PEM file.
+writeSignedObject :: SignedObject a => FilePath -> [X509.SignedExact a] -> IO ()
+writeSignedObject path = writePEMs path . map signedToPEM
+
+-- | Write signed objects to a bytearray in PEM format.
+writeSignedObjectToMemory :: SignedObject a => [X509.SignedExact a] -> B.ByteString
+writeSignedObjectToMemory = pemsWriteBS . map signedToPEM
+
+signedToPEM :: forall a. SignedObject a => X509.SignedExact a -> PEM
+signedToPEM obj = mkPEM (signedObjectName prx) (X509.encodeSignedObject obj)
+  where prx = Proxy :: Proxy a
+
+mkPEM :: String -> B.ByteString -> PEM
+mkPEM name bs = PEM { pemName = name, pemHeader = [], pemContent = bs}
+
+
+-- RSA public keys
+
+newtype RSAPublicKey = RSAPublicKey RSA.PublicKey
+
+instance ASN1Elem e => ProduceASN1Object e RSAPublicKey where
+    asn1s (RSAPublicKey pub) = asn1Container Sequence (n . e)
+      where
+        n = gIntVal (RSA.public_n pub)
+        e = gIntVal (RSA.public_e pub)
+
+instance Monoid e => ParseASN1Object e RSAPublicKey where
+    parse = onNextContainer Sequence $ do
+        IntVal modulus <- getNext
+        IntVal pubexp <- getNext
+        let pub = RSA.PublicKey { RSA.public_size = calculate_modulus modulus 1
+                                , RSA.public_n    = modulus
+                                , RSA.public_e    = pubexp
+                                }
+        return (RSAPublicKey pub)
+      where
+        calculate_modulus n i
+            | (2 ^ (i * 8)) > n = i
+            | otherwise         = calculate_modulus n (i + 1)
diff --git a/tests/CMS/Instances.hs b/tests/CMS/Instances.hs
new file mode 100644
--- /dev/null
+++ b/tests/CMS/Instances.hs
@@ -0,0 +1,468 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | Orphan instances.
+module CMS.Instances
+    ( arbitraryPassword
+    , arbitraryAttributes
+    , arbitraryIntegrityDigest
+    , arbitrarySigVer
+    , arbitraryEnvDev
+    ) where
+
+import           Data.ASN1.Types
+import qualified Data.ByteArray as B
+import           Data.ByteString (ByteString)
+import           Data.X509
+
+import Test.Tasty.QuickCheck
+
+import Crypto.Cipher.Types
+
+import Crypto.Store.CMS
+import Crypto.Store.Error
+
+import X509.Instances
+
+arbitrarySmall :: Gen ByteString
+arbitrarySmall = resize 10 (B.pack <$> arbitrary)
+
+arbitraryPassword :: Gen ByteString
+arbitraryPassword = resize 16 (B.pack <$> asciiChar)
+  where asciiChar = listOf $ choose (0x20,0x7f)
+
+instance Arbitrary ContentInfo where
+    arbitrary = sized $ \n ->
+        if n == 0
+            then DataCI <$> arbitraryMessage
+            else oneof [ DataCI <$> arbitraryMessage
+                       , arbitrarySignedData
+                       , arbitraryEnvelopedData
+                       , arbitraryDigestedData
+                       , arbitraryEncryptedData
+                       , arbitraryAuthenticatedData
+                       , arbitraryAuthEnvelopedData
+                       ]
+      where
+        arbitraryMessage :: Gen ByteString
+        arbitraryMessage = resize 2048 (B.pack <$> arbitrary)
+
+        arbitrarySignedData :: Gen ContentInfo
+        arbitrarySignedData = do
+            alg   <- arbitrary
+            (sigFns, _) <- arbitrarySigVer alg
+            inner <- scale (subtract $ length sigFns) arbitrary
+            signData sigFns inner >>= failIfError
+
+        arbitraryEnvelopedData :: Gen ContentInfo
+        arbitraryEnvelopedData = do
+            oinfo <- arbitrary
+            (alg, key, attrs) <- getCommon
+            (envFns, _) <- arbitraryEnvDev key
+            inner <- scale (subtract $ length envFns) arbitrary
+            envelopData oinfo key alg envFns attrs inner >>= failIfError
+
+        arbitraryDigestedData :: Gen ContentInfo
+        arbitraryDigestedData = do
+            inner <- scale pred arbitrary
+            dt <- arbitrary
+            return $ digestData dt inner
+
+        arbitraryEncryptedData :: Gen ContentInfo
+        arbitraryEncryptedData = do
+            (alg, key, attrs) <- getCommon
+            inner <- scale pred arbitrary
+            failIfError $ encryptData key alg attrs inner
+
+        arbitraryAuthenticatedData :: Gen ContentInfo
+        arbitraryAuthenticatedData = do
+            (oinfo, alg, key, envFns, aAttrs, uAttrs) <- getCommonAuth
+            dig <- arbitrary
+            inner <- scale (subtract $ length envFns) arbitrary
+            generateAuthenticatedData oinfo key alg dig envFns aAttrs uAttrs inner
+                >>= failIfError
+
+        arbitraryAuthEnvelopedData :: Gen ContentInfo
+        arbitraryAuthEnvelopedData = do
+            (oinfo, alg, key, envFns, aAttrs, uAttrs) <- getCommonAuth
+            inner <- scale (subtract $ length envFns) arbitrary
+            authEnvelopData oinfo key alg envFns aAttrs uAttrs inner
+                >>= failIfError
+
+        getCommonAuth :: (HasKeySize params, Arbitrary params)
+                  => Gen ( OriginatorInfo, params, ContentEncryptionKey
+                         , [ProducerOfRI Gen], [Attribute], [Attribute]
+                         )
+        getCommonAuth = do
+            oinfo <- arbitrary
+            (alg, key, uAttrs) <- getCommon
+            aAttrs <- arbitraryAttributes
+            (envFns, _) <- arbitraryEnvDev key
+            return (oinfo, alg, key, envFns, aAttrs, uAttrs)
+
+        getCommon :: (HasKeySize params, Arbitrary params, B.ByteArray key)
+                  => Gen (params, key, [Attribute])
+        getCommon = do
+            alg   <- arbitrary
+            key   <- generateKey alg
+            attrs <- arbitraryAttributes
+            return (alg, key, attrs)
+
+        failIfError :: Either StoreError a -> Gen a
+        failIfError = either (fail . show) return
+
+instance Arbitrary Attribute where
+    arbitrary = do
+        oid  <- arbitraryOID
+        vals <- resize 3 $ listOf1 (OctetString <$> arbitrarySmall)
+        return Attribute { attrType = oid, attrValues = vals }
+
+arbitraryAttributes :: Gen [Attribute]
+arbitraryAttributes = resize 3 arbitrary
+
+instance Arbitrary DigestAlgorithm where
+    arbitrary = elements
+        [ DigestAlgorithm MD2
+        , DigestAlgorithm MD4
+        , DigestAlgorithm MD5
+        , DigestAlgorithm SHA1
+        , DigestAlgorithm SHA224
+        , DigestAlgorithm SHA256
+        , DigestAlgorithm SHA384
+        , DigestAlgorithm SHA512
+        ]
+
+arbitraryIntegrityDigest :: Gen DigestAlgorithm
+arbitraryIntegrityDigest = elements
+    [ DigestAlgorithm MD5
+    , DigestAlgorithm SHA1
+    , DigestAlgorithm SHA224
+    , DigestAlgorithm SHA256
+    , DigestAlgorithm SHA384
+    , DigestAlgorithm SHA512
+    ]
+
+instance Arbitrary MACAlgorithm where
+    arbitrary = (\(DigestAlgorithm alg) -> HMAC alg) <$> arbitraryIntegrityDigest
+
+instance Arbitrary OAEPParams where
+    arbitrary = do
+        alg <- arbitrary
+        mga <- MGF1 <$> arbitrary
+        return OAEPParams { oaepHashAlgorithm = alg
+                          , oaepMaskGenAlgorithm = mga
+                          }
+
+instance Arbitrary PSSParams where
+    arbitrary = do
+        alg <- arbitrary
+        mga <- MGF1 <$> arbitrary
+        len <- choose (1, 30)
+        return PSSParams { pssHashAlgorithm = alg
+                         , pssMaskGenAlgorithm = mga
+                         , pssSaltLength = len
+                         }
+
+instance Arbitrary SignatureAlg where
+    arbitrary = oneof
+        [ pure RSAAnyHash
+
+        , pure $ RSA (DigestAlgorithm MD2)
+        , pure $ RSA (DigestAlgorithm MD5)
+        , pure $ RSA (DigestAlgorithm SHA1)
+        , pure $ RSA (DigestAlgorithm SHA224)
+        , pure $ RSA (DigestAlgorithm SHA256)
+        , pure $ RSA (DigestAlgorithm SHA384)
+        , pure $ RSA (DigestAlgorithm SHA512)
+
+        , RSAPSS <$> arbitrary
+
+        , pure $ DSA (DigestAlgorithm SHA1)
+        , pure $ DSA (DigestAlgorithm SHA224)
+        , pure $ DSA (DigestAlgorithm SHA256)
+
+        , pure $ ECDSA (DigestAlgorithm SHA1)
+        , pure $ ECDSA (DigestAlgorithm SHA224)
+        , pure $ ECDSA (DigestAlgorithm SHA256)
+        , pure $ ECDSA (DigestAlgorithm SHA384)
+        , pure $ ECDSA (DigestAlgorithm SHA512)
+        ]
+
+arbitraryKeyPair :: SignatureAlg -> Gen (PubKey, PrivKey)
+arbitraryKeyPair RSAAnyHash = do
+    (pub, priv) <- arbitraryRSA
+    return (PubKeyRSA pub, PrivKeyRSA priv)
+arbitraryKeyPair (RSA _) = do
+    (pub, priv) <- arbitraryRSA
+    return (PubKeyRSA pub, PrivKeyRSA priv)
+arbitraryKeyPair (RSAPSS _) = do
+    (pub, priv) <- arbitraryRSA
+    return (PubKeyRSA pub, PrivKeyRSA priv)
+arbitraryKeyPair (DSA _) = do
+    (pub, priv) <- arbitraryDSA
+    return (PubKeyDSA pub, PrivKeyDSA priv)
+arbitraryKeyPair (ECDSA _) = do
+    (pub, priv) <- arbitraryNamedEC
+    return (PubKeyEC pub, PrivKeyEC priv)
+
+arbitrarySigVer :: SignatureAlg -> Gen ([ProducerOfSI Gen], ConsumerOfSI Gen)
+arbitrarySigVer alg = sized $ \n -> do
+    (sigFn, verFn) <- onePair
+    otherPairs <- resize (min (pred n) 3) $ listOf onePair
+    sigFns <- shuffle (sigFn : map fst otherPairs)
+    return (sigFns, verFn)
+  where
+    onePair = do
+        (pub, priv) <- arbitraryKeyPair alg
+        chain <- arbitraryCertificateChain pub
+        sAttrs <- oneof [ pure Nothing, Just <$> arbitraryAttributes ]
+        uAttrs <- arbitraryAttributes
+        return (certSigner alg priv chain sAttrs uAttrs, withPublicKey pub)
+
+instance Arbitrary PBKDF2_PRF where
+    arbitrary = elements
+        [ PBKDF2_SHA1
+        , PBKDF2_SHA256
+        , PBKDF2_SHA512
+        ]
+
+instance Arbitrary ContentEncryptionAlg where
+    arbitrary = elements
+        [ CBC DES
+        , CBC DES_EDE3
+        , CBC AES128
+        , CBC AES192
+        , CBC AES256
+        , CBC CAST5
+        , CBC Camellia128
+        , CBC_RC2
+
+        , ECB DES
+        , ECB AES128
+        , ECB AES192
+        , ECB AES256
+        , ECB Camellia128
+
+        , CFB DES
+        , CFB AES128
+        , CFB AES192
+        , CFB AES256
+        , CFB Camellia128
+
+        , CTR Camellia128
+        ]
+
+instance Arbitrary ContentEncryptionParams where
+    arbitrary = arbitrary >>= gen
+      where
+        gen CBC_RC2 = choose (24, 512) >>= generateRC2EncryptionParams
+        gen alg     = generateEncryptionParams alg
+
+instance Arbitrary AuthContentEncryptionAlg where
+    arbitrary = elements
+        [ AUTH_ENC_128
+        , AUTH_ENC_256
+        , CHACHA20_POLY1305
+
+        , CCM AES128
+        , CCM AES192
+        , CCM AES256
+
+        , GCM AES128
+        , GCM AES192
+        , GCM AES256
+        ]
+
+instance Arbitrary AuthContentEncryptionParams where
+    arbitrary = do
+        alg <- arbitrary
+        case alg of
+            AUTH_ENC_128 -> arb3 generateAuthEnc128Params
+            AUTH_ENC_256 -> arb3 generateAuthEnc256Params
+            CHACHA20_POLY1305 -> generateChaChaPoly1305Params
+            CCM c -> do m <- arbitraryM
+                        l <- arbitraryL
+                        generateCCMParams c m l
+            GCM c -> choose (12,16) >>= generateGCMParams c
+      where arb3 fn = do
+                a <- arbitrary; b <- arbitrary; c <- arbitrary
+                fn a b c
+
+arbitraryM :: Gen CCM_M
+arbitraryM = elements
+    [ CCM_M4
+    , CCM_M6
+    , CCM_M8
+    , CCM_M10
+    , CCM_M12
+    , CCM_M14
+    , CCM_M16
+    ]
+
+arbitraryL :: Gen CCM_L
+arbitraryL = elements [ CCM_L2, CCM_L3, CCM_L4 ]
+
+instance Arbitrary KeyDerivationFunc where
+    arbitrary = do
+        salt <- generateSalt 8
+        oneof [ pbkdf2 salt , scrypt salt ]
+      where
+        pbkdf2 salt = do
+            iters <- choose (1,512)
+            pf <- arbitrary
+            return PBKDF2 { pbkdf2Salt           = salt
+                          , pbkdf2IterationCount = iters
+                          , pbkdf2KeyLength      = Nothing
+                          , pbkdf2Prf            = pf
+                          }
+        scrypt salt = do
+            (n, r, p) <- elements [ (16, 1, 1) , (1024, 8, 16) ]
+            return Scrypt { scryptSalt      = salt
+                          , scryptN         = n
+                          , scryptR         = r
+                          , scryptP         = p
+                          , scryptKeyLength = Nothing
+                          }
+
+instance Arbitrary KeyTransportParams where
+    arbitrary = oneof
+        [ pure RSAES
+        , RSAESOAEP <$> arbitrary
+        ]
+
+instance Arbitrary KeyEncryptionParams where
+    arbitrary = oneof
+        [ PWRIKEK <$> arbitrary
+        , return AES128_WRAP
+        , return AES192_WRAP
+        , return AES256_WRAP
+        , return AES128_WRAP_PAD
+        , return AES192_WRAP_PAD
+        , return AES256_WRAP_PAD
+        , return DES_EDE3_WRAP
+        , RC2_WRAP <$> choose (1, 1024)
+        ]
+
+instance Arbitrary OtherKeyAttribute where
+    arbitrary = do
+        oid <- arbitraryOID
+        vals <- resize 3 $ listOf1 (OctetString <$> arbitrarySmall)
+        return OtherKeyAttribute { keyAttrId = oid, keyAttr = vals }
+
+instance Arbitrary KeyIdentifier where
+    arbitrary = do
+        kid <- arbitrarySmall
+        KeyIdentifier kid Nothing <$> arbitrary
+
+arbitraryAgreeParams :: KeyEncryptionParams -> Gen KeyAgreementParams
+arbitraryAgreeParams alg = oneof
+    [ flip StdDH alg <$> arbitraryDigest
+    , flip CofactorDH alg <$> arbitraryDigest
+    ]
+  where
+    arbitraryDigest =
+        elements
+            [ DigestAlgorithm SHA1
+            , DigestAlgorithm SHA224
+            , DigestAlgorithm SHA256
+            , DigestAlgorithm SHA384
+            , DigestAlgorithm SHA512
+            ]
+
+arbitraryEnvDev :: ContentEncryptionKey
+                -> Gen ([ProducerOfRI Gen], ConsumerOfRI Gen)
+arbitraryEnvDev cek = sized $ \n -> do
+    (envFn, devFn) <- onePair
+    otherPairs <- resize (min (pred n) 3) $ listOf onePair
+    envFns <- shuffle (envFn : map fst otherPairs)
+    return (envFns, devFn)
+  where
+    len     = B.length cek
+    onePair = oneof [ arbitraryKT, arbitraryKA, arbitraryKEK, arbitraryPW ]
+
+    arbitraryKT = do
+        (pub, priv) <- arbitraryLargeRSA
+        cert <- arbitrarySignedCertificate (PubKeyRSA pub)
+        ktp  <- arbitrary
+        let envFn = forKeyTransRecipient cert ktp
+            devFn = withRecipientKeyTrans (PrivKeyRSA priv)
+        return (envFn, devFn)
+
+    arbitraryKA = do
+        (pub, priv) <- arbitraryNamedEC
+        cert <- arbitrarySignedCertificate (PubKeyEC pub)
+        kap  <- arbitraryAlg >>= arbitraryAgreeParams
+        let envFn = forKeyAgreeRecipient cert kap
+            devFn = withRecipientKeyAgree (PrivKeyEC priv) cert
+        return (envFn, devFn)
+
+    arbitraryKEK = do
+        kid <- arbitrary
+        es  <- arbitraryAlg
+        key <- generateKey es
+        return (forKeyRecipient key kid es, withRecipientKey key)
+
+    arbitraryPW  = do
+        pwd <- arbitraryPassword
+        kdf <- arbitrary
+        cea <- arbitrary `suchThat` notModeCTR
+        let es = PWRIKEK cea
+        return (forPasswordRecipient pwd kdf es, withRecipientPassword pwd)
+
+    arbitraryAlg
+        | len == 24      = oneof [ return AES128_WRAP
+                                 , return AES192_WRAP
+                                 , return AES256_WRAP
+                                 , return AES128_WRAP_PAD
+                                 , return AES192_WRAP_PAD
+                                 , return AES256_WRAP_PAD
+                                 , return DES_EDE3_WRAP
+                                 , RC2_WRAP <$> choose (1, 1024)
+                                 ]
+        | mod len 8 == 0 = oneof [ return AES128_WRAP
+                                 , return AES192_WRAP
+                                 , return AES256_WRAP
+                                 , return AES128_WRAP_PAD
+                                 , return AES192_WRAP_PAD
+                                 , return AES256_WRAP_PAD
+                                 , RC2_WRAP <$> choose (1, 1024)
+                                 ]
+        | otherwise      = oneof [ return AES128_WRAP_PAD
+                                 , return AES192_WRAP_PAD
+                                 , return AES256_WRAP_PAD
+                                 , RC2_WRAP <$> choose (1, 1024)
+                                 ]
+
+    -- key wrapping in PWRIKEK is incompatible with CTR mode so we must never
+    -- generate this combination
+    notModeCTR params =
+        case getContentEncryptionAlg params of
+            CTR _ -> False
+            _     -> True
+
+instance Arbitrary OriginatorInfo where
+    arbitrary = OriginatorInfo <$> arbitrary <*> arbitrary
+
+instance Arbitrary CertificateChoice where
+    arbitrary = oneof [ CertificateCertificate <$> arbitrary
+                      , CertificateOther <$> arbitrary
+                      ]
+
+instance Arbitrary RevocationInfoChoice where
+    arbitrary = oneof [ RevocationInfoCRL <$> arbitrary
+                      , RevocationInfoOther <$> arbitrary
+                      ]
+
+instance Arbitrary OtherCertificateFormat where
+    arbitrary = do
+        oid  <- arbitraryOID
+        vals <- resize 3 $ listOf1 (OctetString <$> arbitrarySmall)
+        return OtherCertificateFormat { otherCertFormat = oid
+                                      , otherCertValues = vals
+                                      }
+
+instance Arbitrary OtherRevocationInfoFormat where
+    arbitrary = do
+        oid  <- arbitraryOID
+        vals <- resize 3 $ listOf1 (OctetString <$> arbitrarySmall)
+        return OtherRevocationInfoFormat { otherRevInfoFormat = oid
+                                         , otherRevInfoValues = vals
+                                         }
diff --git a/tests/CMS/Tests.hs b/tests/CMS/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/CMS/Tests.hs
@@ -0,0 +1,311 @@
+-- | CMS tests.
+module CMS.Tests (cmsTests) where
+
+import Control.Monad
+
+import qualified Data.ByteString as B
+import           Data.String (fromString)
+import           Data.Maybe (isNothing)
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.CMS
+import Crypto.Store.PKCS8
+import Crypto.Store.X509 (readSignedObject)
+
+import CMS.Instances
+import Util
+
+message :: B.ByteString
+message = fromString "hello, world\r\n"
+
+testKey :: Int -> B.ByteString
+testKey len = B.pack [0 .. toEnum (len - 1)]
+
+hasType :: ContentType -> ContentInfo -> Bool
+hasType t ci = t == getContentType ci
+
+verifyInnerMessage :: B.ByteString -> ContentInfo -> IO ()
+verifyInnerMessage msg ci = do
+    assertBool "unexpected inner type" (hasType DataType ci)
+    let DataCI bs = ci
+    assertEqual "inner message differs" msg bs
+
+dataTests :: TestTree
+dataTests =
+    testGroup "Data"
+        [ testCase "read" $ do
+              cms <- readCMSFile path
+              length cms @?= count
+              forM_ cms (verifyInnerMessage message)
+        , testCase "write" $ do
+              bs <- B.readFile path
+              let ciList = [DataCI message]
+              writeCMSFileToMemory ciList @?= bs
+        ]
+  where path  = testFile "cms-data.pem"
+        count = 1
+
+signedDataTests :: TestTree
+signedDataTests =
+    testCaseSteps "SignedData" $ \step -> do
+        cms <- readCMSFile path
+        assertEqual "unexpected parse count" (length names) (length cms)
+
+        forM_ (zip [0..] cms) $ \(index, ci) -> do
+            let name = names !! index
+
+            step ("verifying " ++ name)
+            assertBool "unexpected type" (hasType SignedDataType ci)
+            let SignedDataCI sd = ci
+            result <- verifySignedData withSignerKey sd
+            assertJust result (verifyInnerMessage message)
+  where path  = testFile "cms-signed-data.pem"
+        names = [ "RSA"
+                , "DSA"
+                , "EC (named curve)"
+                , "EC (explicit prime curve)"
+                , "RSA-PSS"
+                ]
+
+envelopedDataTests :: TestTree
+envelopedDataTests =
+    testGroup "EnvelopedData"
+        [ testKT "KTRI" path3 keys1
+        , testKA "KARI" path4 keys1
+        , test "KEKRI" path1 keys1 withRecipientKey
+        , test "PWRI" path2 keys2 (\_ -> withRecipientPassword pwd)
+        ]
+  where test caseName path keys f = testCaseSteps caseName $ \step -> do
+            cms <- readCMSFile path
+            assertEqual "unexpected parse count" (length keys) (length cms)
+
+            forM_ (zip [0..] cms) $ \(index, ci) -> do
+                let (name, key) = keys !! index
+
+                step ("testing " ++ name)
+                assertBool "unexpected type" (hasType EnvelopedDataType ci)
+                let EnvelopedDataCI ev = ci
+                result <- openEnvelopedData (f key) ev
+                assertRight result (verifyInnerMessage message)
+        testKT caseName path keys = testCaseSteps caseName $ \step -> do
+            let rsaPath = testFile "rsa-unencrypted-pkcs8.pem"
+            [Unprotected priv] <- readKeyFile rsaPath
+
+            cms <- readCMSFile path
+            assertEqual "unexpected parse count" (length modes * length keys) (length cms)
+
+            let pairs = [ (c, m) | c <- map fst keys1, m <- modes ]
+            forM_ (zip pairs cms) $ \((c, m), ci) -> do
+                step ("testing " ++ c ++ " with " ++ m)
+                assertBool "unexpected type" (hasType EnvelopedDataType ci)
+                let EnvelopedDataCI ev = ci
+                result <- openEnvelopedData (withRecipientKeyTrans priv) ev
+                assertRight result (verifyInnerMessage message)
+        testKA caseName path keys = testCaseSteps caseName $ \step -> do
+            let ecdsaKeyPath  = testFile "ecdsa-p256-unencrypted-pkcs8.pem"
+                ecdsaCertPath = testFile "ecdsa-p256-self-signed-cert.pem"
+            [Unprotected priv] <- readKeyFile ecdsaKeyPath
+            [cert] <- readSignedObject ecdsaCertPath
+
+            cms <- readCMSFile path
+            assertEqual "unexpected parse count" (length mds * length keys) (length cms)
+
+            let pairs = [ (c, h) | c <- map fst keys, h <- mds ]
+            forM_ (zip pairs cms) $ \((c, h), ci) -> do
+                step ("testing " ++ c ++ " with " ++ h)
+                assertBool "unexpected type" (hasType EnvelopedDataType ci)
+                let EnvelopedDataCI ev = ci
+                result <- openEnvelopedData (withRecipientKeyAgree priv cert) ev
+                assertRight result (verifyInnerMessage message)
+        path1 = testFile "cms-enveloped-kekri-data.pem"
+        path2 = testFile "cms-enveloped-pwri-data.pem"
+        path3 = testFile "cms-enveloped-ktri-data.pem"
+        path4 = testFile "cms-enveloped-kari-data.pem"
+        pwd   = fromString "dontchangeme"
+        keys2 = [ ("3DES_CBC",             testKey 24)
+                , ("AES128_CBC",           testKey 16)
+                , ("AES192_CBC",           testKey 24)
+                , ("AES256_CBC",           testKey 32)
+                , ("CAST5_CBC (128 bits)", testKey 16)
+                , ("Camellia128_CBC",      testKey 16)
+                , ("RC2 (128 bits)",       testKey 16)
+                ]
+        keys1 = keys2 ++
+                [ ("AES128_ECB",           testKey 16)
+                , ("AES192_ECB",           testKey 24)
+                , ("AES256_ECB",           testKey 32)
+                , ("Camellia128_ECB",      testKey 16)
+                ]
+        modes = [ "RSAES-PKCS1"
+                , "RSAES-OAEP"
+                ]
+        mds   = [ "SHA1"
+                , "SHA224"
+                , "SHA256"
+                , "SHA384"
+                , "SHA512"
+                ]
+
+digestedDataTests :: TestTree
+digestedDataTests =
+    testCaseSteps "DigestedData" $ \step -> do
+        cms <- readCMSFile path
+        assertEqual "unexpected parse count" (length algs) (length cms)
+
+        forM_ (zip [0..] cms) $ \(index, ci) -> do
+            let (name, alg) = algs !! index
+
+            step ("verifying " ++ name)
+            assertBool "unexpected type" (hasType DigestedDataType ci)
+            let DigestedDataCI dd = ci
+                result = digestVerify dd
+            assertJust result (verifyInnerMessage message)
+
+            step ("digesting " ++ name)
+            let ci' = digestData alg (DataCI message)
+            ci @?= ci'
+  where path  = testFile "cms-digested-data.pem"
+        algs  = [ ("MD5",    DigestAlgorithm MD5)
+                , ("SHA1",   DigestAlgorithm SHA1)
+                , ("SHA224", DigestAlgorithm SHA224)
+                , ("SHA256", DigestAlgorithm SHA256)
+                , ("SHA384", DigestAlgorithm SHA384)
+                , ("SHA512", DigestAlgorithm SHA512)
+                ]
+
+encryptedDataTests :: TestTree
+encryptedDataTests =
+    testCaseSteps "EncryptedData" $ \step -> do
+        cms <- readCMSFile path
+        assertEqual "unexpected parse count" (length keys) (length cms)
+
+        forM_ (zip [0..] cms) $ \(index, ci) -> do
+            let (name, key) = keys !! index
+
+            step ("decrypting " ++ name)
+            assertBool "unexpected type" (hasType EncryptedDataType ci)
+            let EncryptedDataCI ed = ci
+                result = decryptData key ed
+            assertRight result (verifyInnerMessage message)
+
+            step ("encrypting " ++ name)
+            let params = edContentEncryptionParams ed
+                ci'    = encryptData key params [] (DataCI message)
+            Right ci @?= ci'
+  where path  = testFile "cms-encrypted-data.pem"
+        keys  = [ ("DES_CBC",              testKey  8)
+                , ("3DES_CBC",             testKey 24)
+                , ("AES128_CBC",           testKey 16)
+                , ("AES192_CBC",           testKey 24)
+                , ("AES256_CBC",           testKey 32)
+                , ("CAST5_CBC (40 bits)",  testKey  5)
+                , ("CAST5_CBC (128 bits)", testKey 16)
+                , ("Camellia128_CBC",      testKey 16)
+                , ("RC2 (40 bits)",        testKey  5)
+                , ("RC2 (64 bits)",        testKey  8)
+                , ("RC2 (128 bits)",       testKey 16)
+                , ("DES_ECB",              testKey  8)
+                , ("AES128_ECB",           testKey 16)
+                , ("AES192_ECB",           testKey 24)
+                , ("AES256_ECB",           testKey 32)
+                , ("Camellia128_ECB",      testKey 16)
+                ]
+
+authEnvelopedDataTests :: TestTree
+authEnvelopedDataTests =
+    testCaseSteps "AuthEnvelopedData" $ \step -> do
+        cms <- readCMSFile path
+        assertEqual "unexpected parse count" count (length cms)
+
+        forM_ (zip [0..] cms) $ \(index, ci) -> do
+            step ("testing vector " ++ show (index :: Int))
+            assertBool "unexpected type" (hasType AuthEnvelopedDataType ci)
+            let AuthEnvelopedDataCI ae = ci
+            result <- openAuthEnvelopedData (withRecipientPassword pwd) ae
+            assertRight result (verifyInnerMessage msg)
+
+            step ("testing encoded vector " ++ show index)
+            let [Just ci'] = pemToContentInfo [] (contentInfoToPEM ci)
+                AuthEnvelopedDataCI ae' = ci'
+            result' <- openAuthEnvelopedData (withRecipientPassword pwd) ae'
+            assertRight result' (verifyInnerMessage msg)
+  where path  = testFile "cms-auth-enveloped-data-rfc6476.pem"
+        pwd   = fromString "password"
+        msg   = fromString "Some test data\NUL"
+        count = 2
+
+propertyTests :: TestTree
+propertyTests = localOption (QuickCheckMaxSize 5) $ testGroup "properties"
+    [ testProperty "marshalling" $ \l ->
+        let bs = writeCMSFileToMemory l
+        in label (sizeRange bs) $ l === readCMSFileFromMemory bs
+    , testProperty "signing" $ \alg ci ->
+        collect alg $ do
+            (sigFns, verFn) <- scale succ (arbitrarySigVer alg)
+            r <- signData sigFns ci
+            let Right (SignedDataCI sd) = r
+            r' <- verifySignedData verFn sd
+            return (Just ci === r')
+    , testProperty "enveloping" $ \alg ci ->
+        collect alg $ do
+            (oinfo, key, envFns, devFn, attrs) <- getCommon alg
+            r <- envelopData oinfo key alg envFns attrs ci
+            let Right (EnvelopedDataCI ev) = r
+            r' <- openEnvelopedData devFn ev
+            return (Right ci === r')
+    , testProperty "digesting" $ \alg ci ->
+        collect alg $
+            let DigestedDataCI dd = digestData alg ci
+             in Just ci === digestVerify dd
+    , testProperty "encrypting" $ \alg ci ->
+        collect alg $ do
+            key <- generateKey alg
+            attrs <- arbitraryAttributes
+            let Right (EncryptedDataCI ed) = encryptData key alg attrs ci
+            return (Right ci === decryptData key ed)
+    , testProperty "authenticating" $ \alg dig ci ->
+        collect alg $ do
+            (oinfo, key, envFns, devFn, uAttrs) <- getCommon alg
+            aAttrs <- if isNothing dig then pure [] else arbitraryAttributes
+            r <- generateAuthenticatedData oinfo key alg dig envFns aAttrs uAttrs ci
+            let Right (AuthenticatedDataCI ad) = r
+            r' <- verifyAuthenticatedData devFn ad
+            return (Right ci === r')
+    , testProperty "enveloping with authentication" $ \alg ci ->
+        collect alg $ do
+            (oinfo, key, envFns, devFn, uAttrs) <- getCommon alg
+            aAttrs <- arbitraryAttributes
+            r <- authEnvelopData oinfo key alg envFns aAttrs uAttrs ci
+            let Right (AuthEnvelopedDataCI ae) = r
+            r' <- openAuthEnvelopedData devFn ae
+            return (Right ci === r')
+    ]
+  where
+    sizeRange bs =
+        let n = B.length bs `div` 1024
+         in show n ++ " .. " ++ show (n + 1) ++ " KB"
+
+getCommon :: HasKeySize params
+          => params
+          -> Gen (OriginatorInfo, B.ByteString, [ProducerOfRI Gen], ConsumerOfRI Gen, [Attribute])
+getCommon alg = do
+    oinfo <- arbitrary
+    key <- generateKey alg
+    (envFns, devFn) <- scale succ (arbitraryEnvDev key)
+    attrs <- arbitraryAttributes
+    return (oinfo, key, envFns, devFn, attrs)
+
+cmsTests :: TestTree
+cmsTests =
+    testGroup "CMS"
+        [ dataTests
+        , signedDataTests
+        , envelopedDataTests
+        , digestedDataTests
+        , encryptedDataTests
+        , authEnvelopedDataTests
+        , propertyTests
+        ]
diff --git a/tests/Cipher/RC2.hs b/tests/Cipher/RC2.hs
new file mode 100644
--- /dev/null
+++ b/tests/Cipher/RC2.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+-- | Test vectors from RFC 2268.
+module Cipher.RC2 (rc2Tests) where
+
+import Data.ByteString (ByteString, pack)
+
+import Crypto.Cipher.Types
+import Crypto.Error
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.Cipher.RC2
+
+import Util
+
+newtype Message = Message ByteString deriving (Show, Eq)
+
+instance Arbitrary Message where
+    arbitrary = sized $ \n -> Message . pack <$> vector (8 * n)
+
+data Vector = Vector
+    { vecEffectiveKeyLength :: Int
+    , vecKey                :: ByteString
+    , vecPlaintext          :: ByteString
+    , vecCiphertext         :: ByteString
+    }
+
+vectors :: [Vector]
+vectors =
+    [ Vector
+        { vecEffectiveKeyLength = 63
+        , vecKey                = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\xeb\xb7\x73\xf9\x93\x27\x8e\xff"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 64
+        , vecKey                = "\xff\xff\xff\xff\xff\xff\xff\xff"
+        , vecPlaintext          = "\xff\xff\xff\xff\xff\xff\xff\xff"
+        , vecCiphertext         = "\x27\x8b\x27\xe4\x2e\x2f\x0d\x49"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 64
+        , vecKey                = "\x30\x00\x00\x00\x00\x00\x00\x00"
+        , vecPlaintext          = "\x10\x00\x00\x00\x00\x00\x00\x01"
+        , vecCiphertext         = "\x30\x64\x9e\xdf\x9b\xe7\xd2\xc2"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 64
+        , vecKey                = "\x88"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\x61\xa8\xa2\x44\xad\xac\xcc\xf0"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 64
+        , vecKey                = "\x88\xbc\xa9\x0e\x90\x87\x5a"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\x6c\xcf\x43\x08\x97\x4c\x26\x7f"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 64
+        , vecKey                = "\x88\xbc\xa9\x0e\x90\x87\x5a\x7f\x0f\x79\xc3\x84\x62\x7b\xaf\xb2"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\x1a\x80\x7d\x27\x2b\xbe\x5d\xb1"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 128
+        , vecKey                = "\x88\xbc\xa9\x0e\x90\x87\x5a\x7f\x0f\x79\xc3\x84\x62\x7b\xaf\xb2"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\x22\x69\x55\x2a\xb0\xf8\x5c\xa6"
+        }
+    , Vector
+        { vecEffectiveKeyLength = 129
+        , vecKey                = "\x88\xbc\xa9\x0e\x90\x87\x5a\x7f\x0f\x79\xc3\x84\x62\x7b\xaf\xb2\x16\xf8\x0a\x6f\x85\x92\x05\x84\xc4\x2f\xce\xb0\xbe\x25\x5d\xaf\x1e"
+        , vecPlaintext          = "\x00\x00\x00\x00\x00\x00\x00\x00"
+        , vecCiphertext         = "\x5b\x78\xd3\xa4\x3d\xff\xf1\xf1"
+        }
+    ]
+
+testCipher :: forall cipher . BlockCipher cipher
+           => String -> [Vector] -> cipher -> TestTree
+testCipher name vec _cipher =
+    testGroup name
+        [ testGroup "properties"
+            [ testProperty "decrypt . encrypt == id" encryptDecryptProperty
+            ]
+        , testGroup "vectors" $ zipWith makeTest [1..] vec
+        ]
+  where
+    initCipher :: BlockCipher cipher => ByteString -> cipher
+    initCipher k = throwCryptoError (cipherInit k)
+
+    encryptDecryptProperty :: TestKey cipher -> Message -> Property
+    encryptDecryptProperty (Key key) (Message msg) =
+        ecbDecrypt ctx (ecbEncrypt ctx msg) === msg
+      where ctx = initCipher key
+
+    makeTest :: Integer -> Vector -> TestTree
+    makeTest i Vector{..} =
+        testGroup (show i)
+            [ testCase "Encrypt" (ecbEncrypt ctx vecPlaintext @?= vecCiphertext)
+            , testCase "Decrypt" (ecbDecrypt ctx vecCiphertext @?= vecPlaintext)
+            ]
+      where
+        ctx = throwCryptoError $
+            rc2WithEffectiveKeyLength vecEffectiveKeyLength vecKey
+
+rc2Tests :: TestTree
+rc2Tests = testCipher "Cipher.RC2" vectors (undefined :: RC2)
diff --git a/tests/KeyWrap/AES.hs b/tests/KeyWrap/AES.hs
new file mode 100644
--- /dev/null
+++ b/tests/KeyWrap/AES.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+-- | Test vectors from RFC 3394 and RFC 5649.
+module KeyWrap.AES (aeskwTests) where
+
+import Data.ByteString (ByteString, pack)
+
+import Crypto.Cipher.AES
+import Crypto.Cipher.Types
+import Crypto.Error
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.KeyWrap.AES
+
+import Util
+
+newtype Message = Message ByteString deriving (Show, Eq)
+
+instance Arbitrary Message where
+    arbitrary = sized $ \n -> Message . pack <$> vector (8 * n)
+
+newtype MessageP = MessageP ByteString deriving (Show, Eq)
+
+instance Arbitrary MessageP where
+    arbitrary = MessageP . pack <$> listOf1 arbitrary
+
+data Vector = Vector
+    { vecKey        :: ByteString
+    , vecPlaintext  :: ByteString
+    , vecCiphertext :: ByteString
+    }
+
+vectors128 :: [Vector]
+vectors128 =
+    [ Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF"
+        , vecCiphertext = "\x1F\xA6\x8B\x0A\x81\x12\xB4\x47\xAE\xF3\x4B\xD8\xFB\x5A\x7B\x82\x9D\x3E\x86\x23\x71\xD2\xCF\xE5"
+        }
+    ]
+
+vectors128P :: [Vector]
+vectors128P = []
+
+vectors192 :: [Vector]
+vectors192 =
+    [ Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF"
+        , vecCiphertext = "\x96\x77\x8B\x25\xAE\x6C\xA4\x35\xF9\x2B\x5B\x97\xC0\x50\xAE\xD2\x46\x8A\xB8\xA1\x7A\xD8\x4E\x5D"
+        }
+    , Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF\x00\x01\x02\x03\x04\x05\x06\x07"
+        , vecCiphertext = "\x03\x1D\x33\x26\x4E\x15\xD3\x32\x68\xF2\x4E\xC2\x60\x74\x3E\xDC\xE1\xC6\xC7\xDD\xEE\x72\x5A\x93\x6B\xA8\x14\x91\x5C\x67\x62\xD2"
+        }
+    ]
+
+vectors192P :: [Vector]
+vectors192P =
+    [ Vector
+        { vecKey        = "\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"
+        , vecPlaintext  = "\xc3\x7b\x7e\x64\x92\x58\x43\x40\xbe\xd1\x22\x07\x80\x89\x41\x15\x50\x68\xf7\x38"
+        , vecCiphertext = "\x13\x8b\xde\xaa\x9b\x8f\xa7\xfc\x61\xf9\x77\x42\xe7\x22\x48\xee\x5a\xe6\xae\x53\x60\xd1\xae\x6a\x5f\x54\xf3\x73\xfa\x54\x3b\x6a"
+        }
+    , Vector
+        { vecKey        = "\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"
+        , vecPlaintext  = "\x46\x6f\x72\x50\x61\x73\x69"
+        , vecCiphertext = "\xaf\xbe\xb0\xf0\x7d\xfb\xf5\x41\x92\x00\xf2\xcc\xb5\x0b\xb2\x4f"
+        }
+    ]
+
+vectors256 :: [Vector]
+vectors256 =
+    [ Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF"
+        , vecCiphertext = "\x64\xE8\xC3\xF9\xCE\x0F\x5B\xA2\x63\xE9\x77\x79\x05\x81\x8A\x2A\x93\xC8\x19\x1E\x7D\x6E\x8A\xE7"
+        }
+    , Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF\x00\x01\x02\x03\x04\x05\x06\x07"
+        , vecCiphertext = "\xA8\xF9\xBC\x16\x12\xC6\x8B\x3F\xF6\xE6\xF4\xFB\xE3\x0E\x71\xE4\x76\x9C\x8B\x80\xA3\x2C\xB8\x95\x8C\xD5\xD1\x7D\x6B\x25\x4D\xA1"
+        }
+    , Vector
+        { vecKey        = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"
+        , vecPlaintext  = "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
+        , vecCiphertext = "\x28\xC9\xF4\x04\xC4\xB8\x10\xF4\xCB\xCC\xB3\x5C\xFB\x87\xF8\x26\x3F\x57\x86\xE2\xD8\x0E\xD3\x26\xCB\xC7\xF0\xE7\x1A\x99\xF4\x3B\xFB\x98\x8B\x9B\x7A\x02\xDD\x21"
+        }
+    ]
+
+vectors256P :: [Vector]
+vectors256P = []
+
+testCipher :: forall cipher . BlockCipher cipher
+           => [Vector] -> [Vector] -> cipher -> TestTree
+testCipher vec vecP cipher =
+    testGroup (cipherName cipher)
+        [ testGroup "properties"
+            [ testProperty "unwrap . wrap == id" wrapUnwrapProperty
+            , testProperty "unwrapPad . wrapPad == id" wrapUnwrapPadProperty
+            ]
+        , testGroup "vectors" $
+            zipWith makeTest  [1..] vec ++ zipWith makeTestP [1..] vecP
+        ]
+  where
+    initCipher :: BlockCipher cipher => ByteString -> cipher
+    initCipher k = throwCryptoError (cipherInit k)
+
+    wrapUnwrapProperty :: TestKey cipher -> Message -> Property
+    wrapUnwrapProperty (Key key) (Message msg) =
+        (wrap ctx msg >>= unwrap ctx) === Right msg
+      where ctx = initCipher key
+
+    makeTest :: Integer -> Vector -> TestTree
+    makeTest i Vector{..} =
+        testGroup (show i)
+            [ testCase "Wrap"   (wrap ctx vecPlaintext @?= Right vecCiphertext)
+            , testCase "Unwrap" (unwrap ctx vecCiphertext @?= Right vecPlaintext)
+            ]
+      where ctx = initCipher vecKey
+
+    wrapUnwrapPadProperty :: TestKey cipher -> MessageP -> Property
+    wrapUnwrapPadProperty (Key key) (MessageP msg) =
+        (wrapPad ctx msg >>= unwrapPad ctx) === Right msg
+      where ctx = initCipher key
+
+    makeTestP :: Integer -> Vector -> TestTree
+    makeTestP i Vector{..} =
+        testGroup ("Pad" ++ show i)
+            [ testCase "Wrap"   (wrapPad ctx vecPlaintext @?= Right vecCiphertext)
+            , testCase "Unwrap" (unwrapPad ctx vecCiphertext @?= Right vecPlaintext)
+            ]
+      where ctx = initCipher vecKey
+
+aeskwTests :: TestTree
+aeskwTests = testGroup "KeyWrap.AES"
+    [ testCipher vectors128 vectors128P (undefined :: AES128)
+    , testCipher vectors192 vectors192P (undefined :: AES192)
+    , testCipher vectors256 vectors256P (undefined :: AES256)
+    ]
diff --git a/tests/KeyWrap/RC2.hs b/tests/KeyWrap/RC2.hs
new file mode 100644
--- /dev/null
+++ b/tests/KeyWrap/RC2.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+-- | Test vectors from RFC 3217.
+module KeyWrap.RC2 (rc2kwTests) where
+
+import           Data.ByteString (ByteString)
+import qualified Data.ByteString as B
+
+import Crypto.Cipher.Types
+import Crypto.Error
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.Cipher.RC2
+import Crypto.Store.Error
+import Crypto.Store.KeyWrap.RC2
+
+import X509.Instances () -- for instance MonadRandom Gen
+import Util
+
+newtype Message = Message ByteString deriving (Show, Eq)
+
+instance Arbitrary Message where
+    arbitrary = Message . B.pack <$> (choose (0, 255) >>= vector)
+
+data Vector = Vector
+    { vecEKL        :: Int
+    , vecKey        :: ByteString
+    , vecPad        :: ByteString
+    , vecIV         :: ByteString
+    , vecPlaintext  :: ByteString
+    , vecCiphertext :: ByteString
+    }
+
+vectors :: [Vector]
+vectors =
+    [ Vector
+        { vecEKL        = 40
+        , vecKey        = "\xfd\x04\xfd\x08\x06\x07\x07\xfb\x00\x03\xfe\xff\xfd\x02\xfe\x05"
+        , vecPad        = "\x48\x45\xcc\xe7\xfd\x12\x50"
+        , vecIV         = "\xc7\xd9\x00\x59\xb2\x9e\x97\xf7"
+        , vecPlaintext  = "\xb7\x0a\x25\xfb\xc9\xd8\x6a\x86\x05\x0c\xe0\xd7\x11\xea\xd4\xd9"
+        , vecCiphertext = "\x70\xe6\x99\xfb\x57\x01\xf7\x83\x33\x30\xfb\x71\xe8\x7c\x85\xa4\x20\xbd\xc9\x9a\xf0\x5d\x22\xaf\x5a\x0e\x48\xd3\x5f\x31\x38\x98\x6c\xba\xaf\xb4\xb2\x8d\x4f\x35"
+        }
+    , Vector
+        { vecEKL        = 128 -- from RFC Errata
+        , vecKey        = "\xfd\x04\xfd\x08\x06\x07\x07\xfb\x00\x03\xfe\xff\xfd\x02\xfe\x05"
+        , vecPad        = "\x48\x45\xcc\xe7\xfd\x12\x50"
+        , vecIV         = "\xc7\xd9\x00\x59\xb2\x9e\x97\xf7"
+        , vecPlaintext  = "\xb7\x0a\x25\xfb\xc9\xd8\x6a\x86\x05\x0c\xe0\xd7\x11\xea\xd4\xd9"
+        , vecCiphertext = "\xf4\xd8\x02\x1c\x1e\xa4\x63\xd2\x17\xa9\xeb\x69\x29\xff\xa5\x77\x36\xd3\xe2\x03\x86\xc9\x09\x93\x83\x5b\x4b\xe4\xad\x8d\x8a\x1b\xc6\x3b\x25\xde\x2b\xf7\x79\x93"
+        }
+    ]
+
+rc2kwTests :: TestTree
+rc2kwTests =
+    testGroup "KeyWrap.RC2"
+        [ testGroup "properties"
+            [ testProperty "unwrap . wrap == id" wrapUnwrapProperty
+            ]
+        , testGroup "vectors" (zipWith makeTest [1..] vectors)
+        ]
+  where
+    initCipher :: Int -> ByteString -> RC2
+    initCipher ekl k = throwCryptoError (rc2WithEffectiveKeyLength ekl k)
+
+    wrapUnwrapProperty :: TestKey RC2 -> TestIV RC2 -> Message -> Gen Property
+    wrapUnwrapProperty (Key key) (IV ivBs) (Message msg) = do
+        ekl <- choose (1, 1024)
+        let ctx = initCipher ekl key
+        wrapped <- wrap ctx iv msg
+        return $ (wrapped >>= unwrap ctx) === Right msg
+      where Just iv = makeIV ivBs
+
+    makeTest :: Integer -> Vector -> TestTree
+    makeTest i Vector{..} =
+        testGroup (show i)
+            [ testCase "Wrap" (doWrap ctx iv vecPlaintext @?= Right vecCiphertext)
+            , testCase "Unwrap" (unwrap ctx vecCiphertext @?= Right vecPlaintext)
+            ]
+      where ctx = initCipher vecEKL vecKey
+            Just iv = makeIV vecIV
+            doWrap = wrap' Left withRandomPad
+            withRandomPad f len
+                | B.length vecPad /= len = Left (InvalidInput "unexpected length")
+                | otherwise              = Right (f vecPad)
diff --git a/tests/KeyWrap/TripleDES.hs b/tests/KeyWrap/TripleDES.hs
new file mode 100644
--- /dev/null
+++ b/tests/KeyWrap/TripleDES.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+-- | Test vectors from RFC 3217.
+module KeyWrap.TripleDES (tripledeskwTests) where
+
+import Data.ByteString (ByteString, pack)
+
+import Crypto.Cipher.TripleDES
+import Crypto.Cipher.Types
+import Crypto.Error
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.KeyWrap.TripleDES
+
+import Util
+
+newtype Message = Message ByteString deriving (Show, Eq)
+
+instance Arbitrary Message where
+    arbitrary = Message . pack <$> vector 24
+
+data Vector = Vector
+    { vecKey        :: ByteString
+    , vecIV         :: ByteString
+    , vecPlaintext  :: ByteString
+    , vecCiphertext :: ByteString
+    }
+
+vectorsEDE3 :: [Vector]
+vectorsEDE3 =
+    [ Vector
+        { vecKey        = "\x25\x5e\x0d\x1c\x07\xb6\x46\xdf\xb3\x13\x4c\xc8\x43\xba\x8a\xa7\x1f\x02\x5b\x7c\x08\x38\x25\x1f"
+        , vecIV         = "\x5d\xd4\xcb\xfc\x96\xf5\x45\x3b"
+        , vecPlaintext  = "\x29\x23\xbf\x85\xe0\x6d\xd6\xae\x52\x91\x49\xf1\xf1\xba\xe9\xea\xb3\xa7\xda\x3d\x86\x0d\x3e\x98"
+        , vecCiphertext = "\x69\x01\x07\x61\x8e\xf0\x92\xb3\xb4\x8c\xa1\x79\x6b\x23\x4a\xe9\xfa\x33\xeb\xb4\x15\x96\x04\x03\x7d\xb5\xd6\xa8\x4e\xb3\xaa\xc2\x76\x8c\x63\x27\x75\xa4\x67\xd4"
+        }
+    ]
+
+vectorsEEE3 :: [Vector]
+vectorsEEE3 = []
+
+vectorsEDE2 :: [Vector]
+vectorsEDE2 = []
+
+vectorsEEE2 :: [Vector]
+vectorsEEE2 = []
+
+testCipher :: forall cipher . BlockCipher cipher => [Vector] -> cipher -> TestTree
+testCipher vectors cipher =
+    testGroup (cipherName cipher)
+        [ localOption (QuickCheckTests 10) $ testGroup "properties"
+            [ testProperty "unwrap . wrap == id" wrapUnwrapProperty
+            ]
+        , testGroup "vectors" (zipWith makeTest [1..] vectors)
+        ]
+  where
+    initCipher :: BlockCipher cipher => ByteString -> cipher
+    initCipher k = throwCryptoError (cipherInit k)
+
+    wrapUnwrapProperty :: TestKey cipher -> TestIV cipher -> Message -> Property
+    wrapUnwrapProperty (Key key) (IV ivBs) (Message msg) =
+        (wrap ctx iv msg >>= unwrap ctx) === Right msg
+      where ctx = initCipher key
+            Just iv = makeIV ivBs
+
+    makeTest :: Integer -> Vector -> TestTree
+    makeTest i Vector{..} =
+        testGroup (show i)
+            [ testCase "Wrap"   (wrap ctx iv vecPlaintext @?= Right vecCiphertext)
+            , testCase "Unwrap" (unwrap ctx vecCiphertext @?= Right vecPlaintext)
+            ]
+      where ctx = initCipher vecKey
+            Just iv = makeIV vecIV
+
+tripledeskwTests :: TestTree
+tripledeskwTests = testGroup "KeyWrap.TripleDES"
+    [ testCipher vectorsEDE3 (undefined :: DES_EDE3)
+    , testCipher vectorsEEE3 (undefined :: DES_EEE3)
+    , testCipher vectorsEDE2 (undefined :: DES_EDE2)
+    , testCipher vectorsEEE2 (undefined :: DES_EEE2)
+    ]
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,26 @@
+-- | cryptostore test suite.
+module Main (main) where
+
+import Test.Tasty
+
+import KeyWrap.AES
+import KeyWrap.TripleDES
+import KeyWrap.RC2
+import Cipher.RC2
+import CMS.Tests
+import PKCS12.Tests
+import PKCS8.Tests
+import X509.Tests
+
+-- | Run the test suite.
+main :: IO ()
+main = defaultMain $ testGroup "cryptostore"
+    [ aeskwTests
+    , tripledeskwTests
+    , rc2kwTests
+    , rc2Tests
+    , cmsTests
+    , x509Tests
+    , pkcs8Tests
+    , pkcs12Tests
+    ]
diff --git a/tests/PKCS12/Instances.hs b/tests/PKCS12/Instances.hs
new file mode 100644
--- /dev/null
+++ b/tests/PKCS12/Instances.hs
@@ -0,0 +1,71 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | Orphan instances.
+module PKCS12.Instances
+    ( arbitraryPassword
+    , arbitraryIntegrityParams
+    , arbitraryPKCS12
+    ) where
+
+import qualified Data.ByteArray as B
+import           Data.ByteString (ByteString)
+import           Data.Semigroup
+
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.PKCS12
+import Crypto.Store.PKCS5
+
+import CMS.Instances
+import PKCS8.Instances ()
+
+arbitrarySmall :: Gen ByteString
+arbitrarySmall = resize 10 (B.pack <$> arbitrary)
+
+arbitraryIntegrityParams :: Gen IntegrityParams
+arbitraryIntegrityParams = (,) <$> arbitraryIntegrityDigest <*> arbitrary
+
+arbitraryPKCS12 :: Password -> Gen PKCS12
+arbitraryPKCS12 pwd = do
+    p <- one
+    ps <- listOf one
+    return (foldr (<>) p ps)
+  where
+    one = oneof [ unencrypted <$> arbitrary
+                , arbitrary >>= arbitraryEncrypted
+                ]
+
+    arbitraryEncrypted sc = do
+        alg <- arbitrary
+        case encrypted alg pwd sc of
+            Left e -> fail (show e)
+            Right aSafe -> return aSafe
+
+instance Arbitrary SafeContents where
+    arbitrary = SafeContents <$> arbitrary
+
+instance Arbitrary info => Arbitrary (Bag info) where
+    arbitrary = do
+        info <- arbitrary
+        attrs <- arbitraryAttributes
+        return Bag { bagInfo = info, bagAttributes = attrs }
+
+instance Arbitrary CertInfo where
+    arbitrary = CertX509 <$> arbitrary
+
+instance Arbitrary CRLInfo where
+    arbitrary = CRLX509 <$> arbitrary
+
+instance Arbitrary SafeInfo where
+    arbitrary = oneof [ KeyBag <$> arbitrary
+                      , PKCS8ShroudedKeyBag <$> arbitraryShrouded
+                      , CertBag <$> arbitrary
+                      , CRLBag <$> arbitrary
+                      --, SecretBag <$> arbitrary
+                      , SafeContentsBag <$> arbitrary
+                      ]
+
+arbitraryShrouded :: Gen PKCS5
+arbitraryShrouded = do
+    alg <- arbitrary
+    bs <- arbitrarySmall -- fake content, tested with PKCS8
+    return PKCS5 { encryptionAlgorithm = alg, encryptedData = bs }
diff --git a/tests/PKCS12/Tests.hs b/tests/PKCS12/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/PKCS12/Tests.hs
@@ -0,0 +1,99 @@
+-- | PKCS #12 tests.
+module PKCS12.Tests (pkcs12Tests) where
+
+import Control.Monad (forM_)
+
+import Data.PEM (pemContent)
+import Data.String (fromString)
+
+import Crypto.Store.PKCS12
+import Crypto.Store.PKCS8
+import Crypto.Store.X509 (readSignedObject)
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Util
+import PKCS12.Instances
+import X509.Instances
+
+testType :: TestName -> String -> TestTree
+testType caseName prefix = testCaseSteps caseName $ \step -> do
+    let fKey  = testFile (prefix ++ "-unencrypted-pkcs8.pem")
+        fCert = testFile (prefix ++ "-self-signed-cert.pem")
+        p12   = testFile (prefix ++ "-pkcs12.pem")
+
+    step "Reading PKCS #12 files"
+    pems <- readPEMs p12
+    length pems @?= length names
+
+    step "Reading private key"
+    [Unprotected key] <- readKeyFile fKey
+
+    step "Reading certificate"
+    certs <- readSignedObject fCert
+
+    forM_ (zip names pems) $ \(name, pem) -> do
+        let r = readP12FileFromMemory (pemContent pem)
+
+        assertRight r $ \integrity ->
+            assertRight (recover pwd integrity) $ \privacy ->
+                assertRight (recover pwd $ unPKCS12 privacy) $ \scs -> do
+                    step ("Testing " ++ name)
+                    recover pwd (getAllSafeKeys scs) @?= Right [key]
+                    getAllSafeX509Certs scs @?= certs
+  where
+    pwd :: Password
+    pwd = fromString "dontchangeme"
+
+    nameIntegrity n = "integrity with " ++ n
+    namePrivacy t n = t ++ " privacy with " ++ n
+
+    integrityNames = map nameIntegrity integrityModes
+    privacyNames t = ("without " ++ t ++ " privacy") :
+                     map (namePrivacy t) privacyModes
+
+    names = [ "without integrity" ] ++ integrityNames ++
+            privacyNames "certificate" ++ privacyNames "private-key"
+
+    integrityModes = [ "SHA-1"
+                     , "SHA-256"
+                     , "SHA-384"
+                     ]
+
+    privacyModes   = [ "aes-128-cbc"
+                     , "PBE-SHA1-RC2-128"
+                     , "PBE-SHA1-RC2-40"
+                     ]
+
+propertyTests :: TestTree
+propertyTests = localOption (QuickCheckMaxSize 5) $ testGroup "properties"
+    [ testProperty "marshalling" $ do
+        pE <- arbitraryPassword
+        c <- arbitraryPKCS12 pE
+        let r = readP12FileFromMemory $ writeUnprotectedP12FileToMemory c
+        return $ Right (Right c) === (recover (fromString "not-used") <$> r)
+    , testProperty "marshalling with authentication" $ do
+        params <- arbitraryIntegrityParams
+        c <- arbitraryPassword >>= arbitraryPKCS12
+        pI <- arbitraryPassword
+        let r = readP12FileFromMemory <$> writeP12FileToMemory params pI c
+        return $ Right (Right (Right c)) === (fmap (recover pI) <$> r)
+    , localOption (QuickCheckTests 20) $ testProperty "converting credentials" $
+        \pChain pKey privKey -> do
+            pwd <- arbitraryPassword
+            chain <- arbitrary >>= arbitraryCertificateChain
+            let cred = (chain, privKey)
+                pkcs12 = fromCredential pChain pKey pwd cred
+                r = pkcs12 >>= recover pwd . toCredential
+            return $ Right (Just cred) === r
+    ]
+
+pkcs12Tests :: TestTree
+pkcs12Tests =
+    testGroup "PKCS12"
+        [ testType "RSA"                        "rsa"
+        , testType "EC (named curve)"           "ecdsa-p256"
+        , propertyTests
+        ]
diff --git a/tests/PKCS8/Instances.hs b/tests/PKCS8/Instances.hs
new file mode 100644
--- /dev/null
+++ b/tests/PKCS8/Instances.hs
@@ -0,0 +1,39 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | Orphan instances.
+module PKCS8.Instances
+    ( arbitraryPassword
+    ) where
+
+import Test.Tasty.QuickCheck
+
+import Crypto.Store.CMS
+import Crypto.Store.PKCS5
+import Crypto.Store.PKCS8
+
+import CMS.Instances
+
+instance Arbitrary PBEParameter where
+    arbitrary = do
+        salt <- generateSalt 8
+        PBEParameter salt <$> choose (1,512)
+
+instance Arbitrary PBES2Parameter where
+    arbitrary = PBES2Parameter <$> arbitrary <*> arbitrary
+
+instance Arbitrary EncryptionScheme where
+    arbitrary = oneof [ PBES2 <$> arbitrary
+                      , PBE_MD5_DES_CBC <$> arbitrary
+                      , PBE_SHA1_DES_CBC <$> arbitrary
+                      , PBE_SHA1_RC4_128 <$> arbitrary
+                      , PBE_SHA1_RC4_40 <$> arbitrary
+                      , PBE_SHA1_DES_EDE3_CBC <$> arbitrary
+                      , PBE_SHA1_DES_EDE2_CBC <$> arbitrary
+                      , PBE_SHA1_RC2_128 <$> arbitrary
+                      , PBE_SHA1_RC2_40 <$> arbitrary
+                      ]
+
+instance Arbitrary PrivateKeyFormat where
+    arbitrary = elements [ TraditionalFormat, PKCS8Format ]
+
+instance Arbitrary a => Arbitrary (FormattedKey a) where
+    arbitrary = FormattedKey <$> arbitrary <*> arbitrary
diff --git a/tests/PKCS8/Tests.hs b/tests/PKCS8/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/PKCS8/Tests.hs
@@ -0,0 +1,101 @@
+-- | PKCS #8 tests.
+module PKCS8.Tests (pkcs8Tests) where
+
+import qualified Data.ByteString as B
+import           Data.String (fromString)
+
+import Crypto.Store.PKCS8
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Util
+import PKCS8.Instances
+
+keyTests :: String -> TestTree
+keyTests prefix =
+    testGroup "PrivateKey"
+        [ testCase "read outer" $ do
+              kOuter <- readKeyFile fOuter
+              length kOuter @?= 1
+        , testCase "read inner" $ do
+              kInner <- readKeyFile fInner
+              length kInner @?= 1
+        , testCase "same key"   $ do
+              kInner <- readKeyFile fInner
+              kOuter <- readKeyFile fOuter
+              assertBool "keys differ" $
+                  let [Unprotected kI] = kInner
+                      [Unprotected kO] = kOuter
+                   in kI == kO
+        , testCase "write outer" $ do
+              bs <- B.readFile fOuter
+              let kOuter = readKeyFileFromMemory bs
+                  [Unprotected kO] = kOuter
+              writeKeyFileToMemory PKCS8Format [kO] @?= bs
+        , testCase "write inner" $ do
+              bs <- B.readFile fInner
+              let kInner = readKeyFileFromMemory bs
+                  [Unprotected kI] = kInner
+              writeKeyFileToMemory TraditionalFormat [kI] @?= bs
+        ]
+  where
+    fInner = testFile (prefix ++ "-unencrypted-trad.pem")
+    fOuter = testFile (prefix ++ "-unencrypted-pkcs8.pem")
+
+encryptedKeyTests :: String -> TestTree
+encryptedKeyTests prefix =
+    testGroup "EncryptedPrivateKey"
+        [ keyTest "PBES1"  "pbes1"  8
+        , keyTest "PBKDF2" "pbkdf2" 7
+        , keyTest "Scrypt" "scrypt" 3
+        ]
+  where
+    pwd = fromString "dontchangeme"
+
+    keyTest name suffix count =
+        let fE = testFile (prefix ++ "-encrypted-" ++ suffix ++ ".pem")
+            fU = testFile (prefix ++ "-unencrypted-pkcs8.pem")
+         in testGroup name
+                [ testCase "read unencrypted" $ do
+                      kU <- readKeyFile fU
+                      length kU @?= 1
+                , testCase "read encrypted"   $ do
+                      kE <- readKeyFile fE
+                      length kE @?= count
+                , testCase "same keys"        $ do
+                      kE <- readKeyFile fE
+                      kU <- readKeyFile fU
+                      assertBool "some keys differ" $
+                          let [Unprotected key] = kU
+                           in all (\(Protected getKey) -> getKey pwd == Right key) kE
+                ]
+
+testType :: TestName -> String -> TestTree
+testType name prefix =
+    testGroup name
+        [ keyTests prefix
+        , encryptedKeyTests prefix
+        ]
+
+propertyTests :: TestTree
+propertyTests = localOption (QuickCheckMaxSize 5) $ testGroup "properties"
+    [ testProperty "marshalling" $ \fmt l ->
+        let r = readKeyFileFromMemory $ writeKeyFileToMemory fmt l
+        in map Right l === map (recover $ fromString "not-used") r
+    , testProperty "marshalling with encryption" $ \es k -> do
+        p <- arbitraryPassword
+        let r = readKeyFileFromMemory <$> writeEncryptedKeyFileToMemory es p k
+        return $ Right [Right k] === (map (recover p) <$> r)
+    ]
+
+pkcs8Tests :: TestTree
+pkcs8Tests =
+    testGroup "PKCS8"
+        [ testType "RSA"                        "rsa"
+        , testType "DSA"                        "dsa"
+        , testType "EC (named curve)"           "ecdsa-p256"
+        , testType "EC (explicit prime curve)"  "ecdsa-epc"
+        , propertyTests
+        ]
diff --git a/tests/Util.hs b/tests/Util.hs
new file mode 100644
--- /dev/null
+++ b/tests/Util.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+-- | Test utilities.
+module Util
+    ( assertJust
+    , assertRight
+    , testFile
+    , TestKey(..)
+    , TestIV(..)
+    ) where
+
+import Data.ByteString (ByteString, pack)
+
+import Crypto.Cipher.Types
+
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+assertJust :: Maybe a -> (a -> Assertion) -> Assertion
+assertJust (Just a) f = f a
+assertJust Nothing  _ = assertFailure "expecting Just but got Nothing"
+
+assertRight :: Show a => Either a b -> (b -> Assertion) -> Assertion
+assertRight (Right b)  f = f b
+assertRight (Left val) _ =
+    assertFailure ("expecting Right but got: Left " ++ show val)
+
+testFile :: String -> FilePath
+testFile name = "tests/files/" ++ name
+
+newtype TestKey cipher = Key ByteString deriving (Show, Eq)
+
+instance Cipher cipher => Arbitrary (TestKey cipher) where
+    arbitrary = Key . pack <$>
+        case cipherKeySize cipher of
+            KeySizeFixed len -> vector len
+            KeySizeRange a b -> choose (a, b) >>= vector
+            KeySizeEnum list -> elements list >>= vector
+      where cipher = undefined :: cipher
+
+newtype TestIV cipher = IV ByteString deriving (Show, Eq)
+
+instance BlockCipher cipher => Arbitrary (TestIV cipher) where
+    arbitrary = IV . pack <$> vector (blockSize cipher)
+      where cipher = undefined :: cipher
diff --git a/tests/X509/Instances.hs b/tests/X509/Instances.hs
new file mode 100644
--- /dev/null
+++ b/tests/X509/Instances.hs
@@ -0,0 +1,267 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-- | Orphan instances.
+module X509.Instances
+    ( arbitraryOID
+    , arbitraryRSA
+    , arbitraryLargeRSA
+    , arbitraryDSA
+    , arbitraryNamedEC
+    , arbitrarySignedCertificate
+    , arbitraryCertificateChain
+    ) where
+
+import           Data.ASN1.Types
+import qualified Data.ByteArray as B
+import           Data.Hourglass
+import           Data.X509
+
+import Test.Tasty.QuickCheck
+
+import           Crypto.Number.Serialize (i2ospOf_)
+import qualified Crypto.PubKey.DSA as DSA
+import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
+import qualified Crypto.PubKey.ECC.Generate as ECC
+import qualified Crypto.PubKey.ECC.Types as ECC
+import qualified Crypto.PubKey.RSA as RSA
+import           Crypto.Random
+
+-- Warning: not a cryptographic implementation, used for tests only
+instance MonadRandom Gen where
+    getRandomBytes n = B.pack <$> vector n
+
+arbitraryOID :: Gen [Integer]
+arbitraryOID = do
+    o1 <- choose (0,6)
+    o2 <- choose (0,15)
+    os <- resize 5 $ listOf (getPositive <$> arbitrary)
+    return (o1 : o2 : os)
+
+arbitraryDN :: Gen DistinguishedName
+arbitraryDN = DistinguishedName <$> resize 5 (listOf1 arbitraryDE)
+  where
+    arbitrarySE = elements [IA5, UTF8]
+    arbitraryDE = (,) <$> arbitraryOID <*> arbitraryCS
+    arbitraryCS = ASN1CharacterString <$> arbitrarySE <*> arbitraryBS
+    arbitraryBS = resize 16 (B.pack <$> listOf1 arbitrary)
+
+instance Arbitrary PubKey where
+    arbitrary = oneof [ PubKeyRSA . fst <$> arbitraryRSA
+                      , PubKeyDSA . fst <$> arbitraryDSA
+                      , PubKeyEC . fst  <$> arbitraryNamedEC
+                      --, PubKeyEC . fst  <$> arbitraryExplicitPrimeCurve
+                      ]
+
+instance Arbitrary PrivKey where
+    arbitrary = oneof [ PrivKeyRSA . snd <$> arbitraryRSA
+                      , PrivKeyDSA . snd <$> arbitraryDSA
+                      , PrivKeyEC . snd  <$> arbitraryNamedEC
+                      , PrivKeyEC . snd  <$> arbitraryExplicitPrimeCurve
+                      ]
+
+arbitraryRSA :: Gen (RSA.PublicKey, RSA.PrivateKey)
+arbitraryRSA = do
+    n <- elements [ 768, 1024 ]     -- enough bits to sign with SHA-512
+    e <- elements [ 3, 0x10001 ]
+    RSA.generate (n `div` 8) e
+
+arbitraryLargeRSA :: Gen (RSA.PublicKey, RSA.PrivateKey)
+arbitraryLargeRSA = do
+    n <- elements [ 1792, 2048 ]    -- enough bits for RSA-OAEP with SHA-512
+    e <- elements [ 3, 0x10001 ]
+    RSA.generate (n `div` 8) e
+
+arbitraryDSA :: Gen (DSA.PublicKey, DSA.PrivateKey)
+arbitraryDSA = do
+    x <- DSA.generatePrivate params
+    let y = DSA.calculatePublic params x
+        priv = DSA.PrivateKey { DSA.private_params = params, DSA.private_x = x }
+        pub = DSA.PublicKey { DSA.public_params = params, DSA.public_y = y }
+    return (pub, priv)
+  where
+    -- DSA parameters were generated using 'openssl dsaparam -C 2048'
+    params = DSA.Params
+        { DSA.params_p = 0x9994B9B1FC22EC3A5F607B5130D314F35FC8D387015A6D8FA2B56D3CC1F13FE330A631DBC765CEFFD6986BDEB8512580BBAD93D56EE7A8997DB9C65C29313FBC5077DB6F1E9D9E6D3499F997F09C8CF8ECC9E5F38DC34C3D656CFDF463893DDF9E246E223D7E5C4E86F54426DDA5DE112FCEDBFB5B6D6F7C76ED190EA1A7761CA561E8E5803F9D616DAFF25E2CCD4011A6D78D5CE8ED28CC2D865C7EC01508BA96FBD1F8BB5E517B6A5208A90AC2D3DCAE50281C02510B86C16D449465CD4B3754FD91AA19031282122A25C68292F033091FCB9DEBDE0D220F81F7EE4AB6581D24BE48204AF3DA52BDB944DA53B76148055395B30954735DC911574D360C953B
+        , DSA.params_g = 0x10E51AEA37880C5E52DD477ED599D55050C47012D038B9E4B3199C9DE9A5B873B1ABC8B954F26AFEA6C028BCE1783CFE19A88C64E4ED6BFD638802A78457A5C25ABEA98BE9C6EF18A95504C324315EABE7C1EA50E754591E3EFD3D33D4AE47F82F8978ABC871C135133767ACC60683F065430C749C43893D73596B12D5835A78778D0140B2F63B32A5658308DD5BA6BBC49CF6692929FA6A966419404F9A2C216860E3F339EDDB49AD32C294BDB4C9C6BB0D1CC7B691C65968C3A0A5106291CD3810147C8A16B4BFE22968AD9D3890733F4AA9ACD8687A5B981653A4B1824004639956E8C1EDAF31A8224191E8ABD645D2901F5B164B4B93F98039A6EAEC6088
+        , DSA.params_q = 0xE1FDFADD32F46B5035EEB3DB81F9974FBCA69BE2223E62FCA8C77989B2AACDF7
+        }
+
+arbitraryNamedEC :: Gen (PubKeyEC, PrivKeyEC)
+arbitraryNamedEC = do
+    name <- arbitraryCurveName
+    let curve = ECC.getCurveByName name
+    pair <- ECC.generate curve
+    let d = ECDSA.private_d (snd pair)
+        priv = PrivKeyEC_Named { privkeyEC_name = name, privkeyEC_priv = d }
+        q = ECDSA.public_q (fst pair)
+        pt = getSerializedPoint curve q
+        pub = PubKeyEC_Named { pubkeyEC_name = name, pubkeyEC_pub = pt }
+    return (pub, priv)
+
+arbitraryExplicitPrimeCurve :: Gen (PubKeyEC, PrivKeyEC)
+arbitraryExplicitPrimeCurve = do
+    curve <- arbitraryPrimeCurve
+    pair <- ECC.generate curve
+    let cc   = ECC.common_curve curve
+        c    = fp curve
+        gen  = getSerializedPoint curve (ECC.ecc_g cc)
+        d    = ECDSA.private_d (snd pair)
+        priv =
+            PrivKeyEC_Prime
+                { privkeyEC_priv      = d
+                , privkeyEC_a         = ECC.ecc_a cc
+                , privkeyEC_b         = ECC.ecc_b cc
+                , privkeyEC_prime     = ECC.ecc_p c
+                , privkeyEC_generator = gen
+                , privkeyEC_order     = ECC.ecc_n cc
+                , privkeyEC_cofactor  = ECC.ecc_h cc
+                , privkeyEC_seed      = 0
+                }
+        q    = ECDSA.public_q (fst pair)
+        pt   = getSerializedPoint curve q
+        pub  =
+            PubKeyEC_Prime
+                { pubkeyEC_pub        = pt
+                , pubkeyEC_a          = ECC.ecc_a cc
+                , pubkeyEC_b          = ECC.ecc_b cc
+                , pubkeyEC_prime      = ECC.ecc_p c
+                , pubkeyEC_generator  = gen
+                , pubkeyEC_order      = ECC.ecc_n cc
+                , pubkeyEC_cofactor   = ECC.ecc_h cc
+                , pubkeyEC_seed       = 0
+                }
+    return (pub, priv)
+  where
+    fp (ECC.CurveFP c) = c
+    fp _               = error "arbitraryExplicitPrimeCurve: assumption failed"
+
+arbitraryCurveName :: Gen ECC.CurveName
+arbitraryCurveName = elements allCurveNames
+
+allCurveNames :: [ECC.CurveName]
+allCurveNames =
+    [ ECC.SEC_p112r1
+    , ECC.SEC_p112r2
+    , ECC.SEC_p128r1
+    , ECC.SEC_p128r2
+    , ECC.SEC_p160k1
+    , ECC.SEC_p160r1
+    , ECC.SEC_p160r2
+    , ECC.SEC_p192k1
+    , ECC.SEC_p192r1
+    , ECC.SEC_p224k1
+    , ECC.SEC_p224r1
+    , ECC.SEC_p256k1
+    , ECC.SEC_p256r1
+    , ECC.SEC_p384r1
+    , ECC.SEC_p521r1
+    , ECC.SEC_t113r1
+    , ECC.SEC_t113r2
+    , ECC.SEC_t131r1
+    , ECC.SEC_t131r2
+    , ECC.SEC_t163k1
+    , ECC.SEC_t163r1
+    , ECC.SEC_t163r2
+    , ECC.SEC_t193r1
+    , ECC.SEC_t193r2
+    , ECC.SEC_t233k1
+    , ECC.SEC_t233r1
+    , ECC.SEC_t239k1
+    , ECC.SEC_t283k1
+    , ECC.SEC_t283r1
+    , ECC.SEC_t409k1
+    , ECC.SEC_t409r1
+    , ECC.SEC_t571k1
+    , ECC.SEC_t571r1
+    ]
+
+primeCurves :: [ECC.Curve]
+primeCurves = filter isPrimeCurve $ map ECC.getCurveByName allCurveNames
+  where isPrimeCurve (ECC.CurveFP _) = True
+        isPrimeCurve _               = False
+
+arbitraryPrimeCurve :: Gen ECC.Curve
+arbitraryPrimeCurve = elements primeCurves
+
+getSerializedPoint :: ECC.Curve -> ECC.Point -> SerializedPoint
+getSerializedPoint curve pt = SerializedPoint (serializePoint pt)
+  where
+    bs = i2ospOf_ (curveSizeBytes curve)
+
+    serializePoint ECC.PointO      = B.singleton 0
+    serializePoint (ECC.Point x y) = B.cons 4 (B.append (bs x) (bs y))
+
+curveSizeBytes :: ECC.Curve -> Int
+curveSizeBytes curve = (ECC.curveSizeBits curve + 7) `div` 8
+
+instance Arbitrary SignatureALG where
+    arbitrary = elements
+        [ SignatureALG HashSHA1   PubKeyALG_RSA
+        , SignatureALG HashMD5    PubKeyALG_RSA
+        , SignatureALG HashMD2    PubKeyALG_RSA
+        , SignatureALG HashSHA256 PubKeyALG_RSA
+        , SignatureALG HashSHA384 PubKeyALG_RSA
+        , SignatureALG HashSHA512 PubKeyALG_RSA
+        , SignatureALG HashSHA224 PubKeyALG_RSA
+
+        , SignatureALG HashSHA1   PubKeyALG_DSA
+        , SignatureALG HashSHA224 PubKeyALG_DSA
+        , SignatureALG HashSHA256 PubKeyALG_DSA
+
+        , SignatureALG HashSHA224 PubKeyALG_EC
+        , SignatureALG HashSHA256 PubKeyALG_EC
+        , SignatureALG HashSHA384 PubKeyALG_EC
+        , SignatureALG HashSHA512 PubKeyALG_EC
+        ]
+
+instance Arbitrary DateTime where
+    arbitrary =
+        let arbitraryElapsed = Elapsed . Seconds <$> choose (1, 100000000)
+         in timeConvert <$> arbitraryElapsed
+
+arbitraryCertificate :: PubKey -> Gen Certificate
+arbitraryCertificate pubKey =
+    Certificate <$> pure 2
+                <*> arbitrary
+                <*> arbitrary
+                <*> arbitraryDN
+                <*> arbitrary
+                <*> arbitraryDN
+                <*> pure pubKey
+                <*> pure (Extensions Nothing)
+
+instance Arbitrary Certificate where
+    arbitrary = arbitrary >>= arbitraryCertificate
+
+instance Arbitrary RevokedCertificate where
+    arbitrary = RevokedCertificate <$> arbitrary
+                                   <*> arbitrary
+                                   <*> pure (Extensions Nothing)
+
+instance Arbitrary CRL where
+    arbitrary = CRL <$> pure 1
+                    <*> arbitrary
+                    <*> arbitraryDN
+                    <*> arbitrary
+                    <*> arbitrary
+                    <*> arbitrary
+                    <*> pure (Extensions Nothing)
+
+arbitrarySignedExact :: (Show a, Eq a, ASN1Object a)
+                     => a -> Gen (SignedExact a)
+arbitrarySignedExact = objectToSignedExactF doSign
+  where
+    doSign _ = (,) <$> arbitrarySig <*> arbitrary
+    arbitrarySig = B.pack <$> vector 16
+
+arbitrarySignedCertificate :: PubKey -> Gen SignedCertificate
+arbitrarySignedCertificate pubKey =
+    arbitraryCertificate pubKey >>= arbitrarySignedExact
+
+instance (Show a, Eq a, ASN1Object a, Arbitrary a) => Arbitrary (SignedExact a) where
+    arbitrary = arbitrary >>= arbitrarySignedExact
+
+arbitraryCertificateChain :: PubKey -> Gen CertificateChain
+arbitraryCertificateChain pubKey = do
+    leaf <- arbitrarySignedCertificate pubKey
+    others <- resize 3 $ listOf (arbitrary >>= arbitrarySignedCertificate)
+    return $ CertificateChain (leaf:others)
diff --git a/tests/X509/Tests.hs b/tests/X509/Tests.hs
new file mode 100644
--- /dev/null
+++ b/tests/X509/Tests.hs
@@ -0,0 +1,70 @@
+-- | X.509 tests.
+module X509.Tests (x509Tests) where
+
+import qualified Data.ByteString as B
+import           Data.X509
+
+import Crypto.Store.X509
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+
+import Util
+import X509.Instances ()
+
+keyTests :: TestName -> String -> Int -> TestTree
+keyTests name prefix count =
+    testGroup name
+        [ testCase "read public key" $ do
+              keys <- readPubKeyFile fKey
+              length keys @?= count
+        , testCase "read certificate" $ do
+              cert <- readSignedObject fCert :: IO [SignedCertificate]
+              length cert @?= 1
+        , testCase "same key" $ do
+              cert <- readSignedObject fCert :: IO [SignedCertificate]
+              keys <- readPubKeyFile fKey
+              assertBool "keys differ" $
+                  let [c] = cert
+                      key = certPubKey (signedObject (getSigned c))
+                   in all (== key) keys
+        , testCase "write certificate" $ do
+              bs <- B.readFile fCert
+              let objs = readSignedObjectFromMemory bs :: [SignedCertificate]
+              writeSignedObjectToMemory objs @?= bs
+        , testCase "write public key" $ do
+              bs <- B.readFile fKey
+              let key = head (readPubKeyFileFromMemory bs)
+              assertBool "first key differs" $
+                  writePubKeyFileToMemory [key] `B.isPrefixOf` bs
+        ]
+  where
+    fCert = testFile (prefix ++ "-self-signed-cert.pem")
+    fKey  = testFile (prefix ++ "-public.pem")
+
+propertyTests :: TestTree
+propertyTests = localOption (QuickCheckMaxSize 5) $ testGroup "properties"
+    [ testProperty "marshalling public keys" $ \keys ->
+          keys === readPubKeyFileFromMemory (writePubKeyFileToMemory keys)
+    , testProperty "marshalling certificates" $ \objs ->
+          asCerts objs === writeReadObjs objs
+    , testProperty "marshalling CRLs" $ \objs ->
+          asCRLs objs === writeReadObjs objs
+    ]
+  where
+    writeReadObjs :: SignedObject a => [SignedExact a] -> [SignedExact a]
+    writeReadObjs = readSignedObjectFromMemory . writeSignedObjectToMemory
+
+    asCerts = id :: [SignedCertificate] -> [SignedCertificate]
+    asCRLs  = id :: [SignedCRL] -> [SignedCRL]
+
+x509Tests :: TestTree
+x509Tests =
+    testGroup "X509"
+        [ keyTests "RSA"                        "rsa"        2
+        , keyTests "DSA"                        "dsa"        1
+        , keyTests "EC (named curve)"           "ecdsa-p256" 1
+--        , keyTests "EC (explicit prime curve)"  "ecdsa-epc"  1
+        , propertyTests
+        ]
diff --git a/tests/files/cms-auth-enveloped-data-rfc6476.pem b/tests/files/cms-auth-enveloped-data-rfc6476.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-auth-enveloped-data-rfc6476.pem
@@ -0,0 +1,15 @@
+-----BEGIN PKCS7-----
+MIHjBgsqhkiG9w0BCRABF6CB0zCB0AIBADFho18CAQCgGwYJKoZIhvcNAQUMMA4E
+CLfrI6dr0gUWAgITiDAjBgsqhkiG9w0BCRADCTAUBggqhkiG9w0DBwQIZpECRWtz
+u5kEGDCjerXY8odQ7EEEromZJvAurk/j81IrozBSBgkqhkiG9w0BBwEwMwYLKoZI
+hvcNAQkQAw8wJDAUBggqhkiG9w0DBwQI0tCBcU09nxEwDAYIKwYBBQUIAQIFAIAQ
+OsYGYUFdAH0RNc1p4VbKEAQUM2Xo8PMHBoYdqEcsbTodlCFAZH4=
+-----END PKCS7-----
+-----BEGIN PKCS7-----
+MIH9BgsqhkiG9w0BCRABF6CB7TCB6gIBADFyo3ACAQCgGwYJKoZIhvcNAQUMMA4E
+COe3h9+CHRLMAgITiDAsBgsqhkiG9w0BCRADCTAdBglghkgBZQMEAQIEEBHZXFIK
+Or8isjBw7/R9bvYEIBg5IifDwiwqpp8qsHckdarYWJzNu0yu0w3Cyx2DlGw3MFsG
+CSqGSIb3DQEHATA8BgsqhkiG9w0BCRADDzAtMB0GCWCGSAFlAwQBAgQQtyUCdoQ8
+WBulMOJAJ+7DBjAMBggrBgEFBQgBAgUAgBCYNg8MeWI2tS0tnhxihR4QBBSIpMGy
+ungbyvkUsOX80Y34AuKyng==
+-----END PKCS7-----
diff --git a/tests/files/cms-data.pem b/tests/files/cms-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-data.pem
@@ -0,0 +1,3 @@
+-----BEGIN CMS-----
+MB0GCSqGSIb3DQEHAaAQBA5oZWxsbywgd29ybGQNCg==
+-----END CMS-----
diff --git a/tests/files/cms-digested-data.pem b/tests/files/cms-digested-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-digested-data.pem
@@ -0,0 +1,27 @@
+-----BEGIN CMS-----
+MFEGCSqGSIb3DQEHBaBEMEICAQAwDAYIKoZIhvcNAgUFADAdBgkqhkiG9w0BBwGg
+EAQOaGVsbG8sIHdvcmxkDQoEEM1RYEXDSwgRKZGd+Ja3VVo=
+-----END CMS-----
+-----BEGIN CMS-----
+MFAGCSqGSIb3DQEHBaBDMEECAQAwBwYFKw4DAhowHQYJKoZIhvcNAQcBoBAEDmhl
+bGxvLCB3b3JsZA0KBBSCJcpSBohk9ZFjXX73CaruHbNZqA==
+-----END CMS-----
+-----BEGIN CMS-----
+MFwGCSqGSIb3DQEHBaBPME0CAQAwCwYJYIZIAWUDBAIEMB0GCSqGSIb3DQEHAaAQ
+BA5oZWxsbywgd29ybGQNCgQczcRQGqOTlmA3SbLea8IWbnraq6CgYvJB35ds7A==
+-----END CMS-----
+-----BEGIN CMS-----
+MGAGCSqGSIb3DQEHBaBTMFECAQAwCwYJYIZIAWUDBAIBMB0GCSqGSIb3DQEHAaAQ
+BA5oZWxsbywgd29ybGQNCgQgu747Zx6FPf4woOYFlDZvJPAvMeok/0ZRdD/WDHPN
+aCI=
+-----END CMS-----
+-----BEGIN CMS-----
+MHAGCSqGSIb3DQEHBaBjMGECAQAwCwYJYIZIAWUDBAICMB0GCSqGSIb3DQEHAaAQ
+BA5oZWxsbywgd29ybGQNCgQwppJgPaHRdFOAXmaxqE4LqWf8xnTAsB2M/HiQTA/2
+QQDpK7cvaFmjVu6jb8iCj5RL
+-----END CMS-----
+-----BEGIN CMS-----
+MIGABgkqhkiG9w0BBwWgczBxAgEAMAsGCWCGSAFlAwQCAzAdBgkqhkiG9w0BBwGg
+EAQOaGVsbG8sIHdvcmxkDQoEQNmRYGENk6X3fR9Sk9k++QSKUPYBvF+nGKHL4FXN
+QVoNz4kKv9BI4MPnsV77ikuw7kWGR3OFLWO8waCyFK+EEu0=
+-----END CMS-----
diff --git a/tests/files/cms-encrypted-data.pem b/tests/files/cms-encrypted-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-encrypted-data.pem
@@ -0,0 +1,64 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBEGBSsOAwIHBAikeD2B
+358Jh6CABAjJSfpa1LuLyAQIY6cX6W8/IhwAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBQGCCqGSIb3DQMHBAiC
+6+NMJ44OmqCABAjW9hA48kkK3AQIh+a+VHqpvWMAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBAgQQ
+mkkKJDu2aiEgb0/ZxJBsYKCABBDcIZ09lh+9CsLaYdR1SgDOAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBFgQQ
+j6MESspPo2oHNS89EDG1aKCABBATA2wR43qFEySZz42vGzGKAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQBKgQQ
+UVdEll0/Q4TaK20aQF9g2qCABBDEiZdmwk+evavQgmw1sComAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBUGCSqGSIb2fQdCCgQI
+GRIlaIuvmQ+ggAQILWj2kKDN1s0ECEiJbc0I7i2cAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBUGCSqGSIb2fQdCCgQI
+WDzAE/e9EsSggAQILkJl9q9FijEECNIDSgZF99sjAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMB8GCyqDCIyaSz0BAQEC
+BBA2iD8nnUhmlqyjEW24xUTFoIAEEDgF21lihlEHVa1D4g17tDYAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBoGCCqGSIb3DQMCMA4C
+AgCgBAjrdywymrZ6FKCABAj2In9qh8hjGAQI6wNZVOTYKTkAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBkGCCqGSIb3DQMCMA0C
+AXgECFYB6AfzFjbOoIAECBJ3vXMNAn34BAjOBiCsA8vj7AAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMBkGCCqGSIb3DQMCMA0C
+AToECFxl2R8EmepkoIAECFnHCd1/a+BdBAj2gVsc+lQZxAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMAkGBSsOAwIGBACggAQI
+wVvzZIHhjREECBpzXeoXbghhAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMA0GCWCGSAFlAwQBAQQA
+oIAEEPq7Jdg9K+WiMDzclI2E3ZUAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMA0GCWCGSAFlAwQBFQQA
+oIAEEF9g2Xr4VpbfRXzmI6Trt6EAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMA0GCWCGSAFlAwQBKQQA
+oIAEEB6dQRYioWisrBYHjAebWgwAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHBqCAMIACAQAwgAYJKoZIhvcNAQcBMAwGCAOiMQUDAQkBBACg
+gAQQ6RwXVWk9R1eE7Jk7CUyMRAAAAAAAAAAAAAA=
+-----END CMS-----
diff --git a/tests/files/cms-enveloped-kari-data.pem b/tests/files/cms-enveloped-kari-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-enveloped-kari-data.pem
@@ -0,0 +1,462 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdWhgdICAQOgUaFPMAkGByqGSM49AgEDQgAE
+Ed6o5AhsIHUjW+EUXgK6gcsSpY5/tAXgBs9j1h9bNhvBquwRih2dDUmJwJRsjNcU
+UvhBljKPEy2ib6WeQnEsojAcBgkrgQUQhkg/AAIwDwYLKoZIhvcNAQkQAwYFADBc
+MFowLjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAl/pm8dLg
+PgAEKPMvSLFKDI5Pc2NAr0tt7Jrhi82TZo6auIhDBjV4R2PCVe+I680iwiowgAYJ
+KoZIhvcNAQcBMBQGCCqGSIb3DQMHBAi9UZzbbgEaHKCABAj/zb76X6Se/gQIrc14
+qkjoFYMAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdKhgc8CAQOgUaFPMAkGByqGSM49AgEDQgAE
+2Y+zAHO5ulufpaH44mk3kGBgfd9vWzq5HkRae8dlaezpaSmc+oKtZlGZXDuC7bEP
+5eluiynuYYfVVgxETZQ7TDAZBgYrgQQBCwAwDwYLKoZIhvcNAQkQAwYFADBcMFow
+LjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAl/pm8dLgPgAE
+KB36kd95/gx1SjDa4+UL4qWXd1RDCqPFhcSLyCJyAIF19WkCc0U3o8QwgAYJKoZI
+hvcNAQcBMBQGCCqGSIb3DQMHBAh5J5a0tWhj+6CABAhq554Xa7EUdwQIgxGzaXwT
+nX4AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdKhgc8CAQOgUaFPMAkGByqGSM49AgEDQgAE
+XFCZOCO50366MGOprn5RzFqhDK/VHN4zCAripWsSE/MJafxfNd8ov7liRj5e/Rwp
+PtA5wysulypS8sL/8RlwUDAZBgYrgQQBCwEwDwYLKoZIhvcNAQkQAwYFADBcMFow
+LjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAl/pm8dLgPgAE
+KEhcaAC5MPm8Xf5YX7kmTTSoO4tG0rRhYqYLwb0pQFVVQlCSbi2JfqMwgAYJKoZI
+hvcNAQcBMBQGCCqGSIb3DQMHBAhe7ZmMpONL5qCABAgcSGlAlLskmAQIZt9VALxf
+dRwAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdKhgc8CAQOgUaFPMAkGByqGSM49AgEDQgAE
+YnNf8skxVk51tvLHYy3hELWfOug40zlob+10nWeYxOBRBBcNmzLiVJ4VcwEDAvQH
+gSG/ftWR/cu8d7npjh/fTDAZBgYrgQQBCwIwDwYLKoZIhvcNAQkQAwYFADBcMFow
+LjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAl/pm8dLgPgAE
+KPB7npEdNX96iMm3saaTkmkYBLwX1IVEgZU2NHqMNm2wvnBpeBouzq0wgAYJKoZI
+hvcNAQcBMBQGCCqGSIb3DQMHBAivrGaWndwuIaCABAisrCnBVtd/0AQI/h/EdI4d
+JHEAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdKhgc8CAQOgUaFPMAkGByqGSM49AgEDQgAE
+WdNVTNdYqeSR5NCdy0s10ipnhzuQEj3/BoGyL2FL8M3fCrQdP36qacziFOoDjbNZ
+8so2s33aXcCglhZ9/AyULTAZBgYrgQQBCwMwDwYLKoZIhvcNAQkQAwYFADBcMFow
+LjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAl/pm8dLgPgAE
+KJPYTTgAItxMFq0Hqx16UDgGM+ZpkCrPyQARYs3XBZcrzqyDzdQY+XUwgAYJKoZI
+hvcNAQcBMBQGCCqGSIb3DQMHBAidhr3XY7svRKCABAgE1skQLauNDQQIvbre4W/d
+nX8AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+NkH2menyiIOiyJMAsSWmBPPU49NrakPUDywzNitZa0Yu00DOTvmggkQP1RR+QSrk
+JEbx6xaCYxFaJNF/2ZgLBDAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+z1bGx7ABf5OxxKDHaoBXkho4PL2+0S5QMIAGCSqGSIb3DQEHATAdBglghkgBZQME
+AQIEEMD/gqzuyP3V8zydroS0AwiggAQQ6yjZFdDXrEn69YEKqIoXywAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+t6fN7Ia3+ySsvomfy1FsAklLIEERQTKoknia/cMMsks/N+G9nijba7W3La6vUGCt
+eAiaYLp3vd0oUmgDKTgawTAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYH3BR
+OSPFufyVQePGfvngsDBMtp/FmgNxMIAGCSqGSIb3DQEHATAdBglghkgBZQMEAQIE
+ENhrWc7aJlKi6KxbEX8vb8iggAQQNauiYNoLDzYsWl8mIvfYHQAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+oIjIxcHzuen6Vrr5qU8Qnb3l1MbCs/F6JK0uSRDOhgDlcCxF3R3LkqQ/eyyM4AjE
+KLY6aei6j/F6hy5rSvLFoDAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYxdAR
+oqPDS7zSUuZny4vrAUsqJt8jcR3MMIAGCSqGSIb3DQEHATAdBglghkgBZQMEAQIE
+EMuSG2Cb05eLWCdAGIu3BcmggAQQE7wDkRaV9w14sPR2iXsaMAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+bXzt6x2ihVjuL4djVxsWWLt60vLl7bsHAx23Zf7UfSIYL6grI2V6iTtFEV4YppnT
+jHq07oU+wsasCYBnrhGrBTAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYy7XB
+WFrsqm7Qs0Gi9jDwbceZQWZvOsH6MIAGCSqGSIb3DQEHATAdBglghkgBZQMEAQIE
+EHERwOSL8JBMOaPAKywIL9WggAQQQHRSxU2L/5pOk9k+1MChQgAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+2pxcjKYQaCqYHJ7NrJkbTdutonvhJ1ZHlayPhTJecw0OtTK5fAx2rwwUsRQaMYeT
+BuWWGCEvkwQkbMIYWhrhUzAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYa3Qe
+EwS1T2VRYvbOaN/tQRCwQfy+066gMIAGCSqGSIb3DQEHATAdBglghkgBZQMEAQIE
+EB4Es/GYPUJ2f3F6BZWL1caggAQQ/0qaUyUkb0cnu0mAzH/IzAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcmhgcYCAQOgUaFPMAkGByqGSM49AgEDQgAE
+Ok8M+RlivdV5+eJMInGlk3bHDp+lCY75r3WG21etOh5zve1MjlxIWW14MZJt7k0E
+jCl1QbeyzX1EB+hZOvxTvzAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEZMFQwUjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQg
+TgAwCMxitIJmcLt07j37L1QzqlGlzSxGJM4Qk11iQ8IwgAYJKoZIhvcNAQcBMB0G
+CWCGSAFlAwQBFgQQg/zLqFBHcCRnkyr9u3F+kqCABBA68WRoFD0KTi05iwYtRoZ1
+AAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+w09hnWB1xWff+NM3YGEioZc9d0bZdLbsj5i5DGRqp7YGmucLntyjXYvSyyaoKZYI
+X3/mgwjkOlpeoPeaEUjngTAVBgYrgQQBCwAwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQg8WuO
+nYYrxosyAxA4FdwxBWuCCN4DWm7QW1v3ohgo7JswgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBFgQQr2Lf2U4dBL5fMJ3yRiL7eKCABBBxV6zUhoCpDylAiZf7FtWRAAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+RkIgB0n1JaTuswpI3CvbFo7HM9esPnajZpjVNEbkIWncwq203luV2poABI1yd3f0
+8ID1LlhSgzVBh4jLIheAOzAVBgYrgQQBCwEwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgHezE
+008dCR0GpVxBjcHOLALlpcBiJYemn5/Gd0Q8hZIwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBFgQQwHGxX3MzxEAb7tF4q5aZZaCABBArUt/vthlUC+CiE5ZF5n17AAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+TM9a8AkCWY+1PPUiv8YRTDP8CkM8TVguegkv37otYosgoLj5S0IJ5qLuVDmIsrEh
+JV/1M7k3jVghCh8Fk4N74zAVBgYrgQQBCwIwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgTUP+
+ohzITwtreuhRAYCS3xgKjC224WJWNWl71e12tdgwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBFgQQcToIk1W67hLKdYiS0Rv0sqCABBDS/GmIldQoA+y0dpNdb1NiAAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+l87IK6nNVNDGFqQ7aEqqfIDhukqCFWubjY4V1Na8GIiaAZ9mv9Flk8bqqEaVI4Z3
+eZiVZ70fYeeXDQK6IP/HSjAVBgYrgQQBCwMwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgst2+
+ObTvFwlaVdcaVUOGT5dNljVOgFss2P/7HdoxzxkwgAYJKoZIhvcNAQcBMB0GCWCG
+SAFlAwQBFgQQ/YZVdo7qn1Ur5IvS8krveaCABBCXs1En2ICNad4m9RUv95aYAAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdGhgc4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+CVsIWAAIOjLr6akT92E3GnGkuGu/uiV/O5CORs0gieMXoIg00XoVXFvF97w3lIR5
+LSeReKZfeCeqg3sHQ5B0YTAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEtMFwwWjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQo
+aNVpE6iz37lKYrP5eFAktgCDq1874/V23eeiVLz8EeUIu4LLL+zkdzCABgkqhkiG
+9w0BBwEwHQYJYIZIAWUDBAEqBBCldPVP1En+B7twjFmIf9gCoIAEEBQeL+Df7Z0V
+01XPBAXNQAkAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+2acvj/2X41n/UnA+AbfP9FdqBsmPKhzSSJWoqjBc+LvvrEFyv/kvRzF5BVpU9lcq
+OoUA899tXIMMqHezDzN/wDAVBgYrgQQBCwAwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQoRj7S
+0EOP5XoG8DQWywlF3Iys25+1UTQq4KZddk4EvvxuUZ5hbw40+TCABgkqhkiG9w0B
+BwEwHQYJYIZIAWUDBAEqBBDy3jMIbY+L6/nhjGQRmpBDoIAEEBSpJEtVjBwaeZuQ
+GPdO3B8AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+BherrKZIQMHjt/tf6gpZ0LI7MkBGoDMPFY9VvgrJ9yn8Fy4NAaXBLL3q2V+maZtY
+HjTVMq+nCTgNU6TRtk5eOzAVBgYrgQQBCwEwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQotN9/
+b6c9aJnElELnDoL8Vl/bD+GbxARw9r61Mffaje2DwvMurVk9ljCABgkqhkiG9w0B
+BwEwHQYJYIZIAWUDBAEqBBBadkWusaPBLljDA2uaOJvioIAEEMK6XkBQQRUfjkzF
+zFyWDVsAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+OCdlCnQEZi1ty887EKww+iZwZUjLZ3IdS+6AAnDLb4D/OpoRobmYD0TT1ihcCYdO
+Hfs2y7WUskWWEpItGdkpDzAVBgYrgQQBCwIwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQoRlHj
+cDX/7zCtOM+ZdjGFepFWH9bfrXopmSINNL9CCH/KHemYSr2ByDCABgkqhkiG9w0B
+BwEwHQYJYIZIAWUDBAEqBBBFdMNJ+RS/WQnalqWS0KG8oIAEEBvHcKhUPjh+JPcq
+Kr6LxYUAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+tL64aZ6HovPynNbmtlIokrDIicvGdomhqjqG+Tv6Ji9Wcr/1PIH/KMjxxaIMV/Ou
+RHR31qHADGry+ZZ/mXizITAVBgYrgQQBCwMwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQomSyh
+09xxkpMeODlDWDLi3jrq4NaVOeQVB/E7JnwN4MJuTHDu40fxDDCABgkqhkiG9w0B
+BwEwHQYJYIZIAWUDBAEqBBD+Ca0nBbH4qbQL+tgVIx8zoIAEEJ5SxJ+Aw8QtKUQG
+DGcE0CAAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+gZKsGO/M7D9GqyaISh/qOOYm3uM8GD/2u9E864FiDdAE+SHPOYG8xgx0kQxxTzfA
+6EvnXbfhWOT4hmiEL++bTjAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+NYzMGrZJ76aNAOtU7dsLIJ0Itg5SbSI3MIAGCSqGSIb3DQEHATAVBgkqhkiG9n0H
+QgoECI7LjKaujpDRoIAECBjJbDivGx2IBAjiOnGDfU6u/gAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+IBqf10T8dNW1hxj9HMSoNIaaZtl8RPZfloQfgihKvYg9aXuE+Q5RdC4GV/f1pLxd
+JgESlcl/3mOBKohqAvMMmjAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYNUnq
+GaHHtDTCdltY4G7FOyTM1oiWP9qoMIAGCSqGSIb3DQEHATAVBgkqhkiG9n0HQgoE
+CBjyxYOtxWvZoIAECLyMrarzIhWlBAh72cydBQhIyAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+rP25FELCbEM5jBWeeU2rzq0sN3aYACNAznb2u9QA+8FYKUeifEPXN4FAmHZWO0BT
+Lb8XF9w84Ra0lA7INV1RmzAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYXzBl
+/qAahtKaYTCUeb3AYTRyBGnk8RVNMIAGCSqGSIb3DQEHATAVBgkqhkiG9n0HQgoE
+CGEmLKtt35HqoIAECAKttXW1125kBAh2WTqti5Y+mAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+rwn4M/Ame+7UEZmJk3NWyLzfnMvt8CyvRDk3/lILCKRGv8yFi+NMxY0Pnkl/w3lJ
+jmtBSTzyl0FH86yVujnbmTAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY4fPV
+R/Fa5/ielrY0nFK3UbU/+FwdKIsHMIAGCSqGSIb3DQEHATAVBgkqhkiG9n0HQgoE
+CA5LtVn2hR9FoIAECAUAcOqFIJEQBAhb3SxGXkA89wAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+vNqu+oATVqFdAiZIOmdcprvBVhSplrBCBvhPkP25O/Z9o+O9xsnpVZE2vAgftwSA
+KjPw8sqERpPiYUGOxLxdmjAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYxYV9
+jFhY6lIpkXN8AWQJQkNiTDXK9XWSMIAGCSqGSIb3DQEHATAVBgkqhkiG9n0HQgoE
+CP4ABapU/5IzoIAECLUree4PSI1DBAgyAi+gSInDhAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+d4LCQ76At7ug9FqIfvw+Kg2IzG3K8jYD53kqVR0awkV3eRIV/CY+nkyI/x68Gc2y
+UOBeE3BykJ9yaARY9ZuQzzAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+AELGlK0LQ4aMsSoIKeMfZcxcGnqwzLXpMIAGCSqGSIb3DQEHATAfBgsqgwiMmks9
+AQEBAgQQs6uUmpJ+F2+POVFx59Gbz6CABBASwae11oKDmhaMFreD14ehAAAAAAAA
+AAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+6r53ZnLlnO/GpWmxUOvb6FMAyVL99UzfwaPd6jGIkV4/D3/NJLoUAEPjwJwUey6C
+NIYyCTlMFertMzhffcgBMzAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYsSSC
+Png7v6TOhSlBD5yjAJLv/sLQTMLkMIAGCSqGSIb3DQEHATAfBgsqgwiMmks9AQEB
+AgQQ6U90bpZgD/3RS5cOJ1YcNqCABBCFcGfBZv8eoqkxM5j3D6zVAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+Cce5H6wStNqCrn17cOPxAP41QTpUufb5aC9/sWUDkDNgpmm4SNl3S5DX0Ouut+EP
+YhBo9YcN9H4jvLUIjWCYpDAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYjI3A
+1ntItCnh+iM/f7sBdkjkcsZSUG0FMIAGCSqGSIb3DQEHATAfBgsqgwiMmks9AQEB
+AgQQxE+F/5gZnVd42/D7Aedt/qCABBBkwbOOfBBol40r2KyPQjNYAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+sjMsUNnWUgIDCJrffiDuZwu9fpq7A6X5qUIJojvFQ4352D3aCXEenPRQA4g1lueU
+clTZ4AEEHsBFK9Kf9OOuFjAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYCd15
+aJPiSbeZi0tpLI4bQcQLO1YnO2ihMIAGCSqGSIb3DQEHATAfBgsqgwiMmks9AQEB
+AgQQ3T5S3rr68icu+5ADc9ybuKCABBALA6cpuh3nLIQ4Vk1VD7JdAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+ng41pqn3itTDqqaQ/6J/owpC8pEUG46nDlZuRe1Kf4dehXB4KXxYilDPx/30iJCr
+wwzOMEqRtCSY3I9nTWYt6TAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY6qCH
+eLq93ENMbdTZ6MnFf+6BZSEDyF0CMIAGCSqGSIb3DQEHATAfBgsqgwiMmks9AQEB
+AgQQmG/EqB4VbniISb63N/G5U6CABBBab0am0A2aCM7JL8XHHg2tAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+OdnaWWIsuEaRaXJQPvhGSbUuQqJjFcEFusalzcbMIidDhpeqQg4mWv72SjzG58vU
+8iH0V8Kjson8PJUNsRRfTDAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+LH4G/It9nl67Zl+enoPNBPZJerfVCMogMIAGCSqGSIb3DQEHATAZBggqhkiG9w0D
+AjANAgE6BAgh8q9PcJRPNKCABAi3G7yWwrgscwQIY7upGF32NTUAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+e1+bTztfsOpSYLn+adO9a1Ooem6HFGvV8bBppQwi9F+kGu1rTVCQx9qUzgmHETDK
+54R1V1V5a2TOUR8xdu1g9TAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYovcD
+YG2K0JYETFJorRoHtgYjKkSi7egYMIAGCSqGSIb3DQEHATAZBggqhkiG9w0DAjAN
+AgE6BAjUb4iafUjk8aCABAhRNyVhrpHzmwQI8XDonroDuXYAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+FoMnzbf1oXZjFOTtqUDsSWymBrNmHzO3gL+m01VIGnkiFj8EejvCxrPNjabce6TC
+chKlhlDLxdhmb8Mtt52DIjAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYkDI+
+rlPoNxu7WoPB9Sgsejhw94pY7oapMIAGCSqGSIb3DQEHATAZBggqhkiG9w0DAjAN
+AgE6BAiwmRI7w+U7w6CABAhqrIOiZHWGUAQIzOlO4nvZn5gAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
++R9I7CJUNznNxDGiC8cR9qIXZyVK/0clHFtZctKwMQeJ8BEzsFtmAiA97sf9H6E3
+4A8YwgrzeQod4TVDJ0yw3jAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY8/8O
+JKxrUwuqe0mvpJewQk5BCmd/TTsnMIAGCSqGSIb3DQEHATAZBggqhkiG9w0DAjAN
+AgE6BAiHKjbBK3DWiaCABAhCm9TeCCBdowQIbAtKUdN4fn4AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+Mhh6OfMp/kG2xWP4P5p443DBYkcgodaH5+P7eaUuKzb+BCfrXuHhVapcWSEWBpYf
+IXUYk6NO8atarZ2ruCaznjAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYIQek
+muKfqohyH3VwQgfnQWBDEgHt8VKnMIAGCSqGSIb3DQEHATAZBggqhkiG9w0DAjAN
+AgE6BAhBAkTfDryDRKCABAicY2DaqZeobAQIpdbjoyUY04cAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+FKrA/1HA1yvJiDynCweQ1qr8BohYtxNa3KjCGZ+fwbzEMB+FP4aqVB9WbRIaE6MO
+cAPnbew9gsoj4aZyJ7Ik5zAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+xEEeQbm5Q2NfSU21+fPj8vsSEG/vq3EAMIAGCSqGSIb3DQEHATANBglghkgBZQME
+AQEEAKCABBDUnRs2t79NIjljXEwG/sAMAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+1EMpS8ICvJ7JKEnf5i9vTmMfK5yA2urR2+Ta3x6rG/flWRU4p8taH7niLkbMkITK
+ADwwoYTidQpBGuozWCayrDAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYM21g
+lr4ePPZtMp5Zy3NtbA73j+idtew6MIAGCSqGSIb3DQEHATANBglghkgBZQMEAQEE
+AKCABBCfRV7RN4fpIH3Yg+NgyCTzAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+98WX7RssBk6i+iQ2TFGU7bf5JzCOrs413QXgWn8ehHpyPYbqECR8sO3yb/ThrpHO
+ypQcFTyAJSha2BHbmXGnCjAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYpeNN
+d4eEr09mBNEFQsBwXPlDV52OW5ybMIAGCSqGSIb3DQEHATANBglghkgBZQMEAQEE
+AKCABBDEYG1WVg9zU+GtID6Z62MVAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+SdXuTrXQjAV8G+iLGPSGUc7J7/aCLT/w+ySjN3c+ps1bTgU9nDnrvZ0GyaJpMNGl
+gqmkDg2Ju4BD6+qoLX4LZzAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYyew3
+isNLa6VVuOLExAbh6/fneOj2HCamMIAGCSqGSIb3DQEHATANBglghkgBZQMEAQEE
+AKCABBDSe/7UBhgGj8KRH39gK5xWAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+RLdf7BdnFnkVG+Q7V9kfmR/FlJTDBd9equqyvQIPeY4/+d6KIkwrjZ1uGRGnOsH+
+L5+uawKIXwqzCDzIF8Nj8jAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYaWsb
+/pW8CgyafVs7KfkXryp33BN7XRmaMIAGCSqGSIb3DQEHATANBglghkgBZQMEAQEE
+AKCABBAncG9TyQN/m4tJYLSBo3qAAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcmhgcYCAQOgUaFPMAkGByqGSM49AgEDQgAE
+Zw0Y6ktKR60a3WTOG72NTQc1IKKaMQTK71GYgOjOq5uVW1lZHglu4GPBrIM9hGt2
+SsFIP5KvoD9ts7Btyt5wEDAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEZMFQwUjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQg
+31p1xsISU0KVexH6MSvU4u+MDhtuYz6ooa/Cky8crDgwgAYJKoZIhvcNAQcBMA0G
+CWCGSAFlAwQBFQQAoIAEEMan0t82ejdJdRnOLHPvi44AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+IVubQBzSV1SSvJk2ab3mxJ7AkxR2UgilIvujEWK4iGBE2iODGULyg5VKRZArZbyK
+OEXOZEBr29QIHjffEuAHLTAVBgYrgQQBCwAwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgxJ7R
+HSSWI3wgOljuIywmFA4SSxOrqa7URLF8az6cvmwwgAYJKoZIhvcNAQcBMA0GCWCG
+SAFlAwQBFQQAoIAEEDjiS0Z7OhplQiXkzZD4DGAAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+6FLvzaA0WyjIgi7epe2VkiCoTVb4ky2E23AG6u4NkDjbvlG/B+MF0TEmtW5SxAJD
+qUI4EghJcGX2BQ4+UgBi3jAVBgYrgQQBCwEwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgfLT2
+rgPa4JlW8TL8YW30M8m+wxACPAMeAT8TDDYO0WAwgAYJKoZIhvcNAQcBMA0GCWCG
+SAFlAwQBFQQAoIAEEG/qcW4KawEUK0HeOpq4ZdEAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+DVwSKP2BsrinZaF4tlt7ZjRq59l05bF6PqY+9Z0fYJ03dXAM8VsSyEAVqTxbHNTj
+nFWE2C8miuA7r7c8M5JuhjAVBgYrgQQBCwIwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgQ134
+rLBOz3ljqJYBuKTzB2BHi2WdJgR3viPkooC/998wgAYJKoZIhvcNAQcBMA0GCWCG
+SAFlAwQBFQQAoIAEEOMBdbXo7mwTCJLyOU6MvtwAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcahgcMCAQOgUaFPMAkGByqGSM49AgEDQgAE
+xGLdCxmNHwUvIU7BICPdFvIo0+owMOU05HygkoMMIQZxvOVTpzS/2GY1wc7SRyeN
+5+fOiZCN7v8E9CSCKsTJDzAVBgYrgQQBCwMwCwYJYIZIAWUDBAEZMFQwUjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQgRoQC
+VMoHH5IbQzg3vS9Aw3mCGQ36uKpWrs6EjSryfxkwgAYJKoZIhvcNAQcBMA0GCWCG
+SAFlAwQBFQQAoIAEEB6FNYftgZazlVG+9VmWGlgAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgdGhgc4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+Mf4KLGdDuedxTSZmmF4J9+2pQh8AOrQJnR4TVmiXLCddrlj2fJPwB4n74fHNmXYe
+T1A35DVGtXUWYZaFbHfh0TAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEtMFwwWjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQo
+aJolf029cD3lfGip8jgUWQoK+FbU1Wh7Ir6Vn6+PDLZVFHch+RsgIjCABgkqhkiG
+9w0BBwEwDQYJYIZIAWUDBAEpBACggAQQZB1VdS5wydzSW/Oow/7MvwAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+zLajMBAMGJiYDQIoeCr+5Pf1m4rVvZPStzJteLEolwrapZXPes5IlBZQ03k/9fab
+vAmcnWjP6murHML2UjKsLTAVBgYrgQQBCwAwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQoLxA2
+jh7WPxVl57x8E/k0DgyWzErCeCuQgNIwS48DtIBoYos5A98KITCABgkqhkiG9w0B
+BwEwDQYJYIZIAWUDBAEpBACggAQQ6a85Gcfmaz8GUP3Vs15n3AAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+MfV71Ca9sLSoOaL/UGNPH8+4r8AG2tGkOtQ0S7g09BCAuhqoztWPThZkcPoFJTkD
+DEojhHUvlRorZFvjFw7NVTAVBgYrgQQBCwEwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQopfRd
+PBICOge2lZsJZPJsxoldBjUCsBOHuEBEPERic0NmCbWWRO8YOjCABgkqhkiG9w0B
+BwEwDQYJYIZIAWUDBAEpBACggAQQkAAwrvxK9Mb9R/MuXaJkYQAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+63G8cn63oUtF1QgvckQ7DdPtD8oRPAYcymF7avKPqzpKbIHm6mFNOparhbNNhaLn
+zJB+w0oQVJe85AL09eEOYzAVBgYrgQQBCwIwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQox/vj
+Z7M5hgXcSmGsqoW8aOCUX8GdPZRFpUhitVnjZ1Q1GDoLZwwM0zCABgkqhkiG9w0B
+BwEwDQYJYIZIAWUDBAEpBACggAQQcX4IWuRvjN7CpVcGkj3izgAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgc6hgcsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+efrLjBFWs/12kCjSqL03WEaPlRtpgTyLpCbPDc29nFBFcIiIR62cRUpZGuo64rAE
+n69T0nmkW1f0gPVeKvbMNTAVBgYrgQQBCwMwCwYJYIZIAWUDBAEtMFwwWjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQo3qaN
+UAxakRXZmNQR0PIxxQg56V5MrhxKtMAVIyQccYPNhsM/Lap74jCABgkqhkiG9w0B
+BwEwDQYJYIZIAWUDBAEpBACggAQQK8er40y4+VxfVyAFHD+eqAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgcGhgb4CAQOgUaFPMAkGByqGSM49AgEDQgAE
+X1veoS45kW4a0IppN/WS/hnK5xPY91Vy2jIw+edev/HiAVaebyN9BapW4YeELHxt
+VYcIXZZ/o4rW/jDcAXUFSzAYBgkrgQUQhkg/AAIwCwYJYIZIAWUDBAEFMEwwSjAu
+MCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY
+hyqSIsSGvtV3T0H9AXWRdlhJI6Ri5eZFMIAGCSqGSIb3DQEHATAMBggDojEFAwEJ
+AQQAoIAEECAF2uY94Anbr0lhM4tYzdoAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+8zN8f1hdBCp7Tk1xVJWEw5WmsdQ+AY0hpnQL9FMvu3/BUKTx59SfTyi+VI8q7wtK
+W+OqqFhY4qb7mSY0qFdttTAVBgYrgQQBCwAwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYSWYc
+zbbIQhLX251LeuCLfHPa9/r+pobmMIAGCSqGSIb3DQEHATAMBggDojEFAwEJAQQA
+oIAEEGMid1kywhmcLeRfdeSIXIMAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+47qJqR+LpLp69XPlUC//5b/pQOrMadqMPJvd7siWpZvRukn5BuC103f2g9BKGt1z
+o/WlNfQkDC/XuyuUYEFlMTAVBgYrgQQBCwEwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYcCIH
+Mrt5GhLAOqbBiLh0LzSBn1okQT72MIAGCSqGSIb3DQEHATAMBggDojEFAwEJAQQA
+oIAEEDyfyYVCDlUQLaWR5WdNwt0AAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+G2RJOMvnDAjbk1QvmNXcXDWus++MXwfu7Rsvp4YaFzLh9Z4gVpqrJjp114VYKfou
+G+AnSSu7vkPyU7Q2uo/LnjAVBgYrgQQBCwIwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQYbPxf
+nTi9+GDRoFAoBnUugDcyzzRW8riXMIAGCSqGSIb3DQEHATAMBggDojEFAwEJAQQA
+oIAEEIJKU14QW/1QFQVqqOL/DroAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxgb6hgbsCAQOgUaFPMAkGByqGSM49AgEDQgAE
+8dNbdrNgOMn0mfcRBiCu4CSN71lr69qTZtLTIK4wyM/8w9FbLG7dT3ohPVBcyvA3
+krJx+nWZ0KGEx/UbgvlYeTAVBgYrgQQBCwMwCwYJYIZIAWUDBAEFMEwwSjAuMCEx
+HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20CCQCX+mbx0uA+AAQY6mbx
+Dm6+bvjDHV9Nt64Z7iu99EGIKmdZMIAGCSqGSIb3DQEHATAMBggDojEFAwEJAQQA
+oIAEEL2k2+wh+Y/7eEwILV26SN0AAAAAAAAAAAAA
+-----END CMS-----
diff --git a/tests/files/cms-enveloped-kekri-data.pem b/tests/files/cms-enveloped-kekri-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-enveloped-kekri-data.pem
@@ -0,0 +1,61 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxOaI3AgEEMAMEATAwCwYJYIZIAWUDBAEZBCDo
+yhBOcZqwXnAfeiaK52eDsGrKOL85GeJrvvcY9FRBmjCABgkqhkiG9w0BBwEwFAYI
+KoZIhvcNAwcECLWFN6cve5wEoIAECM40Ls/E6HzYBAhHv8Jip9pQvAAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBgQ
+8hblrCFtOKfFY9dCk8aI6gPI38gNxb4wgAYJKoZIhvcNAQcBMB0GCWCGSAFlAwQB
+AgQQgKp1jEsgtoJBBCTFFr7ZW6CABBAAueS9/IDW7qkoAAnwJa6sAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxOaI3AgEEMAMEATAwCwYJYIZIAWUDBAEZBCCJ
+KIFyLbTVDm9cRYy8OhTixgVwfO872a/hEec+o1e1cjCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAEWBBAITkDG0u/WajB6DDvRausgoIAEEJdaa0ftXKLB3M5rjxbGEU8A
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxQaI/AgEEMAMEATAwCwYJYIZIAWUDBAEtBCha
+4D9Jno+cmNYSVvWeOM6m2NgfmddUwIgyoWpbOgd727KQWIBV1AudMIAGCSqGSIb3
+DQEHATAdBglghkgBZQMEASoEEMe06a1kNuV6tFSHGF2wefGggAQQPMxzB77x7i8v
+S1HjzJQPwQAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBiA
++8fCiPHEAnws9D7EEaXyprJ5ISGc5WwwgAYJKoZIhvcNAQcBMBUGCSqGSIb2fQdC
+CgQI9WVhp5WQBPaggAQIxOvRdqE49+MECORNVFB5xgh/AAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBhA
+1PEjPE41AFYjPsRETxh1qYmsD81bJoIwgAYJKoZIhvcNAQcBMB8GCyqDCIyaSz0B
+AQECBBB9knlsBp0PF8VO/Vq6I3dMoIAEEC8p1Q7tahAgvESPigwUWUQAAAAAAAAA
+AAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBgc
+46l8i23oY5lls3xc/VZMqO/K/8XUMhkwgAYJKoZIhvcNAQcBMBkGCCqGSIb3DQMC
+MA0CAToECJw92yJUyRuboIAECHT/xgVF+GKdBAhwAQsKUzbU+AAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBhy
+j3gZd5BrG1Uiyck1oCi/lbnFbm5ozFEwgAYJKoZIhvcNAQcBMA0GCWCGSAFlAwQB
+AQQAoIAEELsU0eo7AL8kWQrzaV03dWkAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxOaI3AgEEMAMEATAwCwYJYIZIAWUDBAEZBCAq
+fkHN7tQE8sIHZBwnAlhauh6soXEJ1rHgYZvK4ZZnxjCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEVBACggAQQYJueGyUSbvpvVwZPwuUDSAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxQaI/AgEEMAMEATAwCwYJYIZIAWUDBAEtBCgA
+KHatZAiCdMZkRJ5wqivOUiTtKGmiQtXN3QqJJJ5aLRoqEJkjz3qWMIAGCSqGSIb3
+DQEHATANBglghkgBZQMEASkEAKCABBComLWQrUf7us505ifmrkIvAAAAAAAAAAAA
+AA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQIxMaIvAgEEMAMEATAwCwYJYIZIAWUDBAEFBBg1
+ZjFOAC8jaQGoniHQjMwwg8JAzXZ5uhkwgAYJKoZIhvcNAQcBMAwGCAOiMQUDAQkB
+BACggAQQvuQ6f0WV5jQiaoQDFib11QAAAAAAAAAAAAA=
+-----END CMS-----
diff --git a/tests/files/cms-enveloped-ktri-data.pem b/tests/files/cms-enveloped-ktri-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-enveloped-ktri-data.pem
@@ -0,0 +1,190 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYAS
+MzqnxC+lVY9dcFRygAur41ttkbkparQlMeSq9yXnFA/vs+9wJFFYjl/RKfv76Mp+
+YM90kPizFeXfMIpCZ+r3+xcE0JQPdRiMICtQXugEJcwRH/YOGM37EFG6sJlNaC8i
+VCfO5Uf7zx51064dPkj2aNvgQhX27ZT7dmGWprVbozCABgkqhkiG9w0BBwEwFAYI
+KoZIhvcNAwcECMLnvaBuUHSpoIAECG+eWrhA2KNoBAg3CK4ewLznOQAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYA2
+f8wQrnm1bik0Ayomu5qJmjDaK/dyLLWXROHH31QoF/2dOiuPYWlFgYdY0sRPDHBJ
+lVDtpq0BLBkK8VG4XQ/YhzhGkFNzlTgsrxdMjrd7aBmbQWm5sxsmFrirw2BtVF2I
+tQhFoCAfhBt+b/PitKQBfFW7Ky2MklqGCdqYzJqO8zCABgkqhkiG9w0BBwEwFAYI
+KoZIhvcNAwcECMLV/HRrNrefoIAECDVAEsd2agZbBAg+Ifn0uwVIsgAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYBU
+MLcA5XvSs4vKd7mP1C2b6eauX9EJvhqC33rQreL499JKamowq4DiwfHTuSKOpriV
++2IGVC7waWS2NTItCwbaKzp/vD2GZp/QJM1W3CsYyoL+3NrTHQNrsi1lwIdhhuUX
+SpJA4Ep5Z3cFSf6PzfJJ/O+1lZYvw9EX52LB020wGjCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAECBBArtuzIoHJMe5PxCGdiU4oQoIAEEOWPlgTT81fpjrOFvFMQDtcA
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYB3
+cVqZNKioiVkMnWltmseu68l9iewiVjOWBmN1KMFVniFKWs9yAytReEQn+9UxC2co
+TBtnDEf3F5b49/TGE5r0f35NNPjq5fUEasyHSxEdyQtXoTWSB3dVACHajD04jHCX
++cRcGGdZHvC3tsk4701FBHj9p3wwZAdbL5XzvvYPvDCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAECBBAC2LihEql1ziAXXDoufcIcoIAEEHoRrnR1WOV+rdwzSi3pVbYA
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYAA
+/9BE5hsBwMbQKNE4y5VqvoalUMA9KLmBWGkNkTC9u6gLYbUAIiGM6lPl4tMkQI1Z
+kQI6Mhce9qlcScfvs4+F5S/TdHroojb+63UQegUN5VQP3rO42JVJ9zTA/ai1Qtib
+f4MKtrpwLcNZf3t2VC92pUpijNT3u10MUGCoSOegqzCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAEWBBDESQ54qb/ShKAkOsNIumAcoIAEEHo2fCHz6SHY/Sv8hypILG4A
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYCb
+/ANiTPLkI+P9HKKjyt9JeT5ekyYp5RMREW4OJncVvDtcol4SjAFtcFW2bud+WRvA
+qH80qw7Z0HCwUNcGJVKDCBXEhbLv7ft3Y8ii/i+CrtN07KnK4hnl304U4/B6LbaM
+gwhx3zJU3eA+Qu7Aih9mDS/ZrzFpxmP5cFkdidsiKTCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAEWBBDXgCwZw8Slrzewlyr9pavGoIAEEH4z5teHfvFrCfJ30jzFjC0A
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYBz
+5yZVvclHOaLiwiKD5CLmQA2xpCcGPr5yh/ToKjjgufVuKurl6iNkAYfKbmHDzOyK
+MbbpYtpiK6+AWcO+JtHJZJ2/4P1JYBJ67amdR20BvKm5X15q8Z1K015U2g1iIlUe
+8Jzg2EwalofNGnR8vcBrhnXdaOi1lnhl79QBAbvg5zCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAEqBBDSlU6mD+hEt2wJ77zZ+Mw9oIAEEMCfkTPDAb48ChJkr4pyIIEA
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYCZ
+QhZLfWRQohqi11gGijjooRnoAl1r1Q+qnXUTEy39Z0riCBmMPSyfrxtnKbQq4Ytp
+zUKBxksMsSEJdO0Itn9cfgYoSAaz2j+wLgMMRQhAskNdUCRNY5wj0rQDrJEp4Xr0
+hTovfkk9JjITxQ/rcjcyKPCjMYhaAe96boq0gf5VezCABgkqhkiG9w0BBwEwHQYJ
+YIZIAWUDBAEqBBAMMb42cklST7NGdvqEKnxMoIAEEEHy3lZS0S0aRn7s9ZBZDwUA
+AAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYAK
+FaLR4MUy6vjIf1GMZ/ljgEjijfF0eV8m5sML+1SJY09/WZ02g4DDZ56eEVlHV8SP
+GnkxU98apWm7LBj0acNsQtuTGgNRCxsiuxVtqmFx/6wcW26T5Hnz3qgFFGuXQz3G
+6DamGqw5DcZhcKsa5rKcGE9LV84ih7U8648TbjTO3jCABgkqhkiG9w0BBwEwFQYJ
+KoZIhvZ9B0IKBAgBIxu0kwTj/aCABAgesQ0q46GB7wQIKigIcZYvAwEAAAAAAAAA
+AAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYAu
+zqzksttaYanHXGNdA1lbrdJMDO19mqIIq0bNuNnNRhi8gkVqiaYSx4jWWihynxvp
+EYj2+VQ9DSYWAN6A5A4RoA5xm7pitXl/lhBdgpe6fCLFwXs5HY8LKkqzIn3MMWOf
+j1vCuZjuUZT1yvZuhnGOk5cua468OHZjF3SCjdCxAjCABgkqhkiG9w0BBwEwFQYJ
+KoZIhvZ9B0IKBAh4qFkOcAm0eaCABAhUZQxFQ+ja7wQIhxDypyzXMeYAAAAAAAAA
+AAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYAp
+7601Ats7XLYQopjKo4X92vK1vpqiV9fo8qcAd6Fo61SEgAPTV4ynHOYzUjDHCTbv
+zFQ6M80t5igMpppmcMc6Rs1y/atpMIWYMTDvCFxLN9iYvwbwq5xuTgsKpBs2s/OJ
+FRDkSOEsBWGHhlaa0JDzfSqPFxO0UnlpkaWxhm56yzCABgkqhkiG9w0BBwEwHwYL
+KoMIjJpLPQEBAQIEELe9YYPj5Ndvaj2thJc4oZaggAQQySVpbfUQPXsnKZ2j3Clj
+DwAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYCf
+M58lS+xD60ThkV/KlhskCQbzYa5tgoMbQaRF1Z5PQoCWH+iueOH83DkwOiatBcX4
+4gb46G6aItDCMyG18v3t0x/DnE+FAEsxucG5iQYBjW+AhaK0O6H1/q7i6j9je46A
+L2YPjK638yb5y34IRcJJWSFqe31SLQdVAIP/fIFGizCABgkqhkiG9w0BBwEwHwYL
+KoMIjJpLPQEBAQIEEOQN2AAuDs3ZKl2RLJb9F0aggAQQFQ2VgsBc8tufBxFTJPlw
+RAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYBY
+EtN5fKcxlW4w+XFlXXCuU+MKgxn6hgDXyfOSNaInIvlTvJ+H1RZfykFo7FdzPvNn
+VLVoegnJSEwdxe/zdl3sxN/V8LIS3fs4mCXQIyA5tsrYHjTHMYojCTmzpWvy2/JF
+L2auV3+kjaIve1uEeIxk0oHmPmIPiMDb13eoLvdJ/DCABgkqhkiG9w0BBwEwGQYI
+KoZIhvcNAwIwDQIBOgQIQ0hQOhF2kRyggAQIJPO3RK4NI88ECE1+ZS8fRrBhAAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYBi
+KZHomluvWpI3YchpacGbj7OFsB6Q5sETL8WzJAiLAgoYOwvWpVYb2PbgVkEpGgs1
+a6xEyz2afKXKdPEc4DK3sGfnM3Lz9Txz8In3d1jAQBXLZl8EoaZQUO5VeD6IvYil
+GU8aPgr+Snu09kKqYdkyNDlCUvqP1ncYNqmFLYNAkjCABgkqhkiG9w0BBwEwGQYI
+KoZIhvcNAwIwDQIBOgQIlrnglIwapsuggAQI3kKGphwDsXEECC1RfvxIP2+aAAAA
+AAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYBq
+P+U1MaRiR82vFKuH6y/apL26VHmkoxNlsODr1UxSdqpLLnk2NHUcef2J6ZL/NsCQ
+J84yKvcWzw6hqS9II9e4KK51MNWjf40T/fWOfDHiVZ0/xc+7AEhiA7jdow2Oiw8F
+FQBZmob310njFzCWpyoJv06I6WhI/2qEuzkBZpjlbzCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEBBACggAQQdRO+ksa4ijjcxJCg53aCdQAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYAg
+vYGS4TlPOZ9ttdCX6p8D/JMEoajtCe0HeTLsax2Skdz9VnEyhrGUhqfy43qrraO2
+N7+o/tHCGa7BpFvM2UfzsF/0KJZsyv9G7txUBEEhYmjMVsZ2QYwAnIgTH95ucWnY
+We04KNRqbxV52EUs1LgOZSLTzbLrvGXPvtsSBKVzzDCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEBBACggAQQyePr0+JPuJ2d0zaw1HqCeAAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYAE
+RECUSsy4tlI9UsIPxUEx6GRBeb0YhlZP6HqUwU2zx76/cm5M3aD6n8XTcNkl8mxG
+7OhGi0884x3uDjihq3YzLgLKTgQ5htnu8frGtAMe4s8MkZ7i3ECBM4HdSLNesvwY
+hyJ2fYatRRaTCvYVGTcQ+mrIvRRSYVokLRZzwFqpGjCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEVBACggAQQnWARpt02jOj8d2M70gXM4QAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYBv
+7rTFST/R7PDsDVm17MQArHteoRUewIAVkbWkd8iB5FFJXSXGxtIs+DtpUqq91z0k
+O5hL4wIzRYUebVvE9XMot2kZQ2PAymX5B1EM0MRc2XFrBOi1Vkq6jCEdow4yedNF
+UL3IhtT9YTErSvK+2c2r/GeBy5oAM+qosCZpk9c2kDCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEVBACggAQQ2QnNQVPLo9Um6plRZP9kogAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYB7
+eybuQWl10p2u2XcrGTHeMYFCvcYNO4NLxFogIRppPRehQhtXGtO38FhjYuj0eNZO
+XP7oaJJFuVTir+YDZdhmUuuDgYKAWmpVEiAhqQBh5ZX6k8HY+h1vdCxs6M27ZXyG
+zsihIjxXoJLZh7yXcZMHJOrl2Oi7bDXRSQ/70JPktDCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEpBACggAQQ5dd51/ShE4xyqxr2v/QcPgAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYBq
+ZKmAhfw2n/2PlcbL6jbMB2PNmX0mu/DTqOssmBExIUqq1R31RlOZ5X799R9XUGMk
+0CzsolL06nhNFSeFK9wqkbljpA6X2UoVKhMe+xc6YNuDa4EGLNfE+hvmiJtVuTir
+HHB6Ke/xRXrPTH713yYQFnJUq7JV3YK0r/yCqO23+jCABgkqhkiG9w0BBwEwDQYJ
+YIZIAWUDBAEpBACggAQQ76KVR7qeUnzHe+A0Q0zyWgAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEBBQAEgYBZ
+OPbl/h6GxeJE5K+5T4cKBaBd9gDOhkXssk8ahr0ykZkJj6XXEUaae9qFO1wmljDe
+bcrMkFtiIwcT74A/qO3sStqEzFtb2qKxqNQg3doFRGQe5H85B7yQI1sPoVUVrvy+
+4YPj2pUNdUXMvCWB/rQTyu6ZN4rnVzuC4GnuXIwoPjCABgkqhkiG9w0BBwEwDAYI
+A6IxBQMBCQEEAKCABBD5WfiWV6XIOjnWwW5F+H91AAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQAxgcgwgcUCAQAwLjAhMR8wHQYJKoZIhvcNAQkB
+FhB0ZXN0QGV4YW1wbGUuY29tAgkA9w3rWSwpN5swDQYJKoZIhvcNAQEHMAAEgYAY
+fXUCemSM4z3r4HVcPtqtgHf61MXgH9yoMT2N9MFyolYSvPjUMhrzKHgcmbWmXWG4
+Msj3Kp3HknvlT680Sf2PCLSuemJ55OAc4b20Y+E0kawK/Y7Kk6662Ic/4nAANYCT
+OPMPuM+qHtRFiCyS7ley4SdvtfkUqa2ylpWjLudiATCABgkqhkiG9w0BBwEwDAYI
+A6IxBQMBCQEEAKCABBAnhuYJW+U0RMPg47sdKrbyAAAAAAAAAAAAAA==
+-----END CMS-----
diff --git a/tests/files/cms-enveloped-pwri-data.pem b/tests/files/cms-enveloped-pwri-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-enveloped-pwri-data.pem
@@ -0,0 +1,48 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxaaNnAgEAoBsGCSqGSIb3DQEFDDAOBAjGmdvq
+OEXsfgICCAAwIwYLKoZIhvcNAQkQAwkwFAYIKoZIhvcNAwcECBqgquyBZfiyBCCB
+e8Znem9EJyy35ISVMOTFu1zPV6F2Ct+GzQWZLtVl3TCABgkqhkiG9w0BBwEwFAYI
+KoZIhvcNAwcECJpn34+AbNP3oIAECOTmA1ktv97ZBAjeeFTRB8REdgAAAAAAAAAA
+AAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxcqNwAgEAoBsGCSqGSIb3DQEFDDAOBAg459s2
+DKHMTwICCAAwLAYLKoZIhvcNAQkQAwkwHQYJYIZIAWUDBAECBBB5IgIXJlXboBd5
+0Ul1hadfBCBmiHy9CnC6/EfTVW8P6WuY7HxO5M9uFpSV7DwNqqra0jCABgkqhkiG
+9w0BBwEwHQYJYIZIAWUDBAECBBC9IWSavohVALU9BchvHMY/oIAEEE+4aWPxtady
+Yv+HK8CNj+AAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxcqNwAgEAoBsGCSqGSIb3DQEFDDAOBAhMLyN3
+9SEyeQICCAAwLAYLKoZIhvcNAQkQAwkwHQYJYIZIAWUDBAEWBBDGLmHK9CHO4OVf
+MSaHJ+FaBCACC4FO2u1LYSpaFmGUMRvL3muwJrm4ZTtx/o0/siV7XTCABgkqhkiG
+9w0BBwEwHQYJYIZIAWUDBAEWBBC/6PTDboiGM4pcWVHGMtzYoIAEEE6ZQvaKhESN
+cVjWjOW3m5AAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxgYOjgYACAQCgGwYJKoZIhvcNAQUMMA4ECJEM
+2A6yA3hOAgIIADAsBgsqhkiG9w0BCRADCTAdBglghkgBZQMEASoEECfPlNUFouJ0
+0Udxr18xzuAEMAEaXXJinmZg6eAMWQbgmRavVidgxq1M0hm1YSnDSsl+onNA1okI
+FC3updaHmEPpJTCABgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBCbfmx6jQ7nNTqb
+t98Phnm+oIAEEC50vNvKr11yid3OHO/N68wAAAAAAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxYqNgAgEAoBsGCSqGSIb3DQEFDDAOBAhgcukM
+kY+kAAICCAAwJAYLKoZIhvcNAQkQAwkwFQYJKoZIhvZ9B0IKBAgx5KKRkXFNHQQY
+mfBZc9TEe7xuV3NUmt1SCpndkttyibGlMIAGCSqGSIb3DQEHATAVBgkqhkiG9n0H
+QgoECAweaGrM2MHCoIAECC8nHwrprn5KBAiAVC6ryHwFrwAAAAAAAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxdKNyAgEAoBsGCSqGSIb3DQEFDDAOBAjE4jm+
+WpxP1AICCAAwLgYLKoZIhvcNAQkQAwkwHwYLKoMIjJpLPQEBAQIEEHxIjOP4CIXt
+mT9aFIQEK4sEINlZLo9mVM/SoJ8f+HbGolEzKiof+yb6tDZVb3Qz2Mx+MIAGCSqG
+SIb3DQEHATAfBgsqgwiMmks9AQEBAgQQYuHF+dgR4zM+zPT6Oa0yPqCABBDDyZL/
+47IMaP6UDK1SNJKkAAAAAAAAAAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHA6CAMIACAQMxZqNkAgEAoBsGCSqGSIb3DQEFDDAOBAjKpTDQ
+/9Z3LgICCAAwKAYLKoZIhvcNAQkQAwkwGQYIKoZIhvcNAwIwDQIBOgQIxvVzbjAh
+9lkEGKYC5xLJK6gZ0xCt/8V2+pi5bubgOAzryDCABgkqhkiG9w0BBwEwGQYIKoZI
+hvcNAwIwDQIBOgQI66/5xUVNrc6ggAQIRVTFbqLaTjsECH6CEr5uv0DJAAAAAAAA
+AAAAAA==
+-----END CMS-----
diff --git a/tests/files/cms-signed-data.pem b/tests/files/cms-signed-data.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/cms-signed-data.pem
@@ -0,0 +1,125 @@
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwgAYJKoZIhvcNAQcB
+oIAkgAQOaGVsbG8sIHdvcmxkDQoAAAAAAACgggIUMIICEDCCAXmgAwIBAgIJAPcN
+61ksKTebMA0GCSqGSIb3DQEBCwUAMCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhh
+bXBsZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMxMDkzNTE0WjAhMR8wHQYJ
+KoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQC0EbaQpZ9hPWNReZsOWuQPdZ/DSsoONN2ONKzAzYeCAy0RuK61z5zG
+ehb4Np4/VLNJ1js1eVqq/N+0I8u/Mk1pEzgekb9cuoh25vmHOKxuvA3X9DvvOZS+
+vWRrs1pcpkO656k5Emw2HmUeSnbZVHQ7SlDfMs2NfwhUAaR2mgTmIwIDAQABo1Aw
+TjAdBgNVHQ4EFgQU/JTTK/+8Q86nWQwPMu+1PpT6Bg4wHwYDVR0jBBgwFoAU/JTT
+K/+8Q86nWQwPMu+1PpT6Bg4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOB
+gQCfbJlP9Y26k7fg/FV1ZTrlf2qYITgC1kottFrSJKSQRj3rWioOIxVaJo3Sw9V5
+XUag0LP0nyUspoSRHxvQXgqUnQrjhX7k/Qe9vVScpXZKw3GSY1vHCM7xD71o2N3g
+irECdLJDjnIw3grnHUI4kVdARMkCkVaEtnGADSGqei1AZzGCAb0wggG5AgEBMC4w
+ITEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbQIJAPcN61ksKTebMAsG
+CWCGSAFlAwQCAaCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3
+DQEJBTEPFw0xODA3MjIwNjIyNTlaMC8GCSqGSIb3DQEJBDEiBCC7vjtnHoU9/jCg
+5gWUNm8k8C8x6iT/RlF0P9YMc81oIjB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFl
+AwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqG
+SIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIB
+KDANBgkqhkiG9w0BAQEFAASBgGbOWcUKMOVsuSHVJyepwfaECpLKJTQH2VDpyVbS
+/CLsmwsGtRQJEIOAVV9OezHHvngtChaCyIuwYmaP17qeN0R7ZvthN5+WCX2DuNUJ
+ucSxEfivSx8zwjns7M3944jUbn4zE33ltk4bjWgryOIASKm8snLgftewhNOU2wKP
+Ar8JAAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwgAYJKoZIhvcNAQcB
+oIAkgAQOaGVsbG8sIHdvcmxkDQoAAAAAAACgggLWMIIC0jCCAo+gAwIBAgIJAIRv
+DyrolgpNMAsGCWCGSAFlAwQDAjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1w
+bGUuY29tMB4XDTE4MDcwMTA5MzUxNFoXDTE4MDczMTA5MzUxNFowITEfMB0GCSqG
+SIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTCCAbYwggErBgcqhkjOOAQBMIIBHgKB
+gQC7Wk5JfPvqyloNxI6wTHy/MAH7ELUs2mlcjF4/+ZAeKbsAOV/m408Bq8+3UrLu
+SQkdt3GP2xZdQb4G+ALUJI2gSQ7RvgDY5ORh1+AKOTwJKM0rguCwtlw0Nb4wxrgb
+sCigYDFMv5Zu8zEwqZaXL14M4N1VU3YztLZQIo+UiSgONwIVANylBkcI+baMiQNo
+w1/dYmCB7NDHAoGAMouAkCn9WUZhh+VR2cnNwBZLTi+D4VBpEhCGhyvNpQlD9l88
+Ji2MKjO3eAv0o3AWCFcGSWjTzaxDCKfnY2CiEHZ8Eog6y8E9fVVTyhea1Di8g3Uv
+qSY9mR5m8SQ4l1WiMcEl6FTUIOFQNP48QfeOjLazc30ALVITVSmVsdUGwTkDgYQA
+AoGAf8tbGLnVhmy7MOIiRd2o8AH6OImp9xy6Zikbib6chsJoEMIxfHEqZ+v+5AFx
+gUht0BThsrhYvuOgeMKNd4D8MPPOMPfCoVIprHvEUpsGYArR4LRR3BWWfH2p4n1i
+l26Tnz4IFapCTIUkIiV/RokWUoTLwES+48LoeW7adNz1Id6jUDBOMB0GA1UdDgQW
+BBRMA7NzBEkpRRlo/9Y7os6U4sJjATAfBgNVHSMEGDAWgBRMA7NzBEkpRRlo/9Y7
+os6U4sJjATAMBgNVHRMEBTADAQH/MAsGCWCGSAFlAwQDAgMwADAtAhUAoLfr7SzK
+6rBM8F3swsns0ElLXRcCFBEoZvR3RDx/X1Q4CvlE6nXQdLF1MYIBaTCCAWUCAQEw
+LjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tAgkAhG8PKuiWCk0w
+CwYJYIZIAWUDBAIBoIHkMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI
+hvcNAQkFMQ8XDTE4MDcyMjA2MjI1OVowLwYJKoZIhvcNAQkEMSIEILu+O2cehT3+
+MKDmBZQ2byTwLzHqJP9GUXQ/1gxzzWgiMHkGCSqGSIb3DQEJDzFsMGowCwYJYIZI
+AWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYI
+KoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMC
+AgEoMAsGCWCGSAFlAwQDAgQvMC0CFGMQG4Ti+cPlCkXhPNChuLXMZbSCAhUAk/am
+O8zvARpcfg+Weq++PJLzOm8AAAAAAAA=
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwgAYJKoZIhvcNAQcB
+oIAkgAQOaGVsbG8sIHdvcmxkDQoAAAAAAACgggGNMIIBiTCCAS+gAwIBAgIJAJf6
+ZvHS4D4AMAoGCCqGSM49BAMCMCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBs
+ZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMxMDkzNTE0WjAhMR8wHQYJKoZI
+hvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEdiq+DUTenjFcFQr7vbN9rcwuYkiR9kOxipGxnBFcntPNHGxt5WAENXLdmPpj
+hlIH90ruPGzngpMQKgNxcu8QBKNQME4wHQYDVR0OBBYEFFPacwYxY7UVPo4lqM6c
+72CnWEdXMB8GA1UdIwQYMBaAFFPacwYxY7UVPo4lqM6c72CnWEdXMAwGA1UdEwQF
+MAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgC8D3epwlGR7JF9+v0grNhBeRkTfSqcIH
+OUR+Z9QKVqACIQDQZArMqLAZO/q5XWnCOmurIjuHR05sQRKTcoxBFr7nNzGCAX8w
+ggF7AgEBMC4wITEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbQIJAJf6
+ZvHS4D4AMAsGCWCGSAFlAwQCAaCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB
+MBwGCSqGSIb3DQEJBTEPFw0xODA3MjIwNjIyNTlaMC8GCSqGSIb3DQEJBDEiBCC7
+vjtnHoU9/jCg5gWUNm8k8C8x6iT/RlF0P9YMc81oIjB5BgkqhkiG9w0BCQ8xbDBq
+MAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3
+DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggq
+hkiG9w0DAgIBKDAKBggqhkjOPQQDAgRGMEQCIFnydUTsOcJuwn9+EAcsL1N7MxQD
+gBKPMKY0H0idZsb9AiBFuYfwgxWnedMjHXE0ZswL8Z0jmdQoQqRvOjlD0kcwuQAA
+AAAAAA==
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwgAYJKoZIhvcNAQcB
+oIAkgAQOaGVsbG8sIHdvcmxkDQoAAAAAAACgggKCMIICfjCCAiOgAwIBAgIJAKAS
+PKiQipNtMAoGCCqGSM49BAMCMCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBs
+ZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMxMDkzNTE0WjAhMR8wHQYJKoZI
+hvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMIIBSzCCAQMGByqGSM49AgEwgfcCAQEw
+LAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////MFsE
+IP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57PrvVV2
+mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR
+8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84z
+V2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVR
+AgEBA0IABJGPZIpCgSCPPr4phDHNPhrKhmBNMIFhZjcSJomXmiTRyohTtIFo9036
+pNVzMIyNS+t+WTzAOYqaB2tVq4sf+XSjUDBOMB0GA1UdDgQWBBSC8kx1ORBUpK2y
+IZjT95j80H+cezAfBgNVHSMEGDAWgBSC8kx1ORBUpK2yIZjT95j80H+cezAMBgNV
+HRMEBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDZoc3hPRA/zknb6/PZGamT4MQn
+PBvwW/0MaPiDBw2JJwIhAPBSJdgVyZuyJlzyhDFVwwodd6UxBRuQ+rXatKcA4k1p
+MYIBfzCCAXsCAQEwLjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29t
+AgkAoBI8qJCKk20wCwYJYIZIAWUDBAIBoIHkMBgGCSqGSIb3DQEJAzELBgkqhkiG
+9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE4MDcyMjA2MjI1OVowLwYJKoZIhvcNAQkE
+MSIEILu+O2cehT3+MKDmBZQ2byTwLzHqJP9GUXQ/1gxzzWgiMHkGCSqGSIb3DQEJ
+DzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYI
+KoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIH
+MA0GCCqGSIb3DQMCAgEoMAoGCCqGSM49BAMCBEYwRAIgUNgm3ZqPzXnroKk7iuXt
+vKzEEcwUdHK6L4GJ98Qz2CkCIDUcs93FD13W3WuyGFS6r3Oj5LlDs4MsOZnDbnIo
+yva4AAAAAAAA
+-----END CMS-----
+-----BEGIN CMS-----
+MIAGCSqGSIb3DQEHAqCAMIACAQExDTALBglghkgBZQMEAgEwgAYJKoZIhvcNAQcB
+oIAkgAQOaGVsbG8sIHdvcmxkDQoAAAAAAACgggIUMIICEDCCAXmgAwIBAgIJAPcN
+61ksKTebMA0GCSqGSIb3DQEBCwUAMCExHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhh
+bXBsZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMxMDkzNTE0WjAhMR8wHQYJ
+KoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQC0EbaQpZ9hPWNReZsOWuQPdZ/DSsoONN2ONKzAzYeCAy0RuK61z5zG
+ehb4Np4/VLNJ1js1eVqq/N+0I8u/Mk1pEzgekb9cuoh25vmHOKxuvA3X9DvvOZS+
+vWRrs1pcpkO656k5Emw2HmUeSnbZVHQ7SlDfMs2NfwhUAaR2mgTmIwIDAQABo1Aw
+TjAdBgNVHQ4EFgQU/JTTK/+8Q86nWQwPMu+1PpT6Bg4wHwYDVR0jBBgwFoAU/JTT
+K/+8Q86nWQwPMu+1PpT6Bg4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOB
+gQCfbJlP9Y26k7fg/FV1ZTrlf2qYITgC1kottFrSJKSQRj3rWioOIxVaJo3Sw9V5
+XUag0LP0nyUspoSRHxvQXgqUnQrjhX7k/Qe9vVScpXZKw3GSY1vHCM7xD71o2N3g
+irECdLJDjnIw3grnHUI4kVdARMkCkVaEtnGADSGqei1AZzGCAe0wggHpAgEBMC4w
+ITEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbQIJAPcN61ksKTebMAsG
+CWCGSAFlAwQCAaCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3
+DQEJBTEPFw0xODA3MjIwNjIyNTlaMC8GCSqGSIb3DQEJBDEiBCC7vjtnHoU9/jCg
+5gWUNm8k8C8x6iT/RlF0P9YMc81oIjB5BgkqhkiG9w0BCQ8xbDBqMAsGCWCGSAFl
+AwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMA4GCCqG
+SIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG9w0DAgIB
+KDA9BgkqhkiG9w0BAQowMKANMAsGCWCGSAFlAwQCAaEaMBgGCSqGSIb3DQEBCDAL
+BglghkgBZQMEAgGiAwIBXgSBgCBmZlgPvc6GpULvkccID/ybILnVh1Zcu5DnhvOz
+isNXtcm3TW8xu9+rLps95FfNrb0akRVRdrHMdOlwwV/y6gl7P7c5lpcijQnIIFae
+5j6YV7LDJNGpnssxkQyF+Kj3wTMdvf+0yChjCSQ9QFkNK/lUZTO9LhXc783xhAO5
+7EQwAAAAAAAA
+-----END CMS-----
diff --git a/tests/files/dsa-encrypted-pbes1.pem b/tests/files/dsa-encrypted-pbes1.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-encrypted-pbes1.pem
@@ -0,0 +1,80 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcTAbBgkqhkiG9w0BBQMwDgQI/aS3q8Ny9UYCAggABIIBUFshzMyw/YNj9r0a
+44vQzZtQ2Xc2aRqQnRJuERmPZiMIUrc41Ej/wGRMJAPItBZJkVPGCOrkgYmyyHK5
+bsyIVoZfCUTfOU5J7rJVdzayQeE1cWjWpcBQVSEMH0k2GctKHOeGN8vzXjVq6hXh
+4XchC/0n5EWVtlbTH2s2nuMSjUY9CQeaodIlW7cuAk5AE97OrQGWK+0BRrHZrbhw
+bjR2fuJVPrPUuUrsuZK7PyKrOpUB8s1GE/2UQ4taCa+eBZfbkD4C82e7kI6v6ZJx
+ECnIVhvA/2I2qic4D7ml7rIlp3Zizs7pXkeoWgTpWrGaUXxQr26jmGWDYJ8WiulW
+h5c9Wo4G3V7xUoYssQu29qHnrB7IQT2dbrL64JbaFjcOXbXlrJ99FOmrZhtXsCL7
+vRipmnrumDaYMj61+petkln9ScRmhl2rP36/nGFIlwJxmJeUQw==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcTAbBgkqhkiG9w0BBQowDgQIr3nft7PJIm4CAggABIIBUBSoegLJo8BpcE+5
+3AAnl7cib9JD4g3wsnVKWc36+32uU4dg+cPgN6/E9ULRvKX9PM1U6MZwUb3M0QAs
+IuZ5UkkW1iNHmxuQoD2EZpBzTM0ykaDq8O/3dRdpmBNrRAW7tiPZxtffgySjzShA
+m/tSH/w2jydZswsP32ZDAgevPG317fHwMVeaI1Gz7DRPxRIRpwgy6HuovwK9N4+W
+jT7hNdvA4iybnFSiYGt36nHlaEjEmkayPAXTcLtW/p/4ctsYr1O2Uq24l2bkZZMH
+abyjm4BYrJV/gV91p/C+wggOCsXBBVF9Awf6YIXC6XF4C327/Ov3ORXJQh2C7ANO
+QGOSqCVZo6o1tlP62XKRmuDDiObl0TwuVG5cyQac/triWyPKtvAEinyv/OM4lCue
+7az07xON9WnpwF3suDpZfx8aKlqNPRhJIce4mE2Wjp65H+5U9A==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcDAcBgoqhkiG9w0BDAEBMA4ECMngjuURplazAgIIAASCAU77yI2EF+UXyyDN
+wp4oJRKrpXSeJsRLV9LzgUMVanmVSJNdkjAilQGE8refAAXzIcV/DKf7jmLgT6t6
+34vyDu5MQTKX9Sq/35RZDgQt0epNRl8x9HkzJXbMwXpjxqN8St5fW3CG7klGyV4+
+hIb4LfRXxHLajdhwJzLDo6hmE2TIYCMGZYWApObVMfms8qDN0ulYxPF0O7UtpNy8
+sT5uvN1/CVUJmG2pfygbdZ/tBkTJLNzbt060EVXBIdUgAkohMq4v2Bm6dM+3FQMi
+abC6rLDlf/Yhleaa0nCG7GUpBDD77AIr6mJ67TYg+//vJa2WWwTc5DOk1+Ooz1NJ
+V1Fj7y3EmG9zBb1EboOBo59/VtWhpAwp73x9QidzZ+RhZqvFFHBuOE2TUxbGlZqO
+w6Cqg6hVz/dJkfbrjQ3I0HFPQPHJLTmmZbqMvuliv2h4g5YM
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcDAcBgoqhkiG9w0BDAECMA4ECB2W/D5ZtLxUAgIIAASCAU46SZU3kLb1v26l
++yAlWkhOypf3Bq5z2w/qy2eRsK827S3FvanOUrjG07VicYJc7wssCvbC61HNxyDT
+4HCvx2tD1VeHtzUcP6/clLwb9ARVNtoA7oLlfloXpTa6WiMbXgYSP3p7CQCvfIX2
+HYnihuFNoMjnZw08GHvy/jEVe7HuN7VSHJ/QSXsv/nIWWRdHZRaZYtLAjYwHH7iT
+z+0D02yiLjN9P5q5dtvUBnwBTrWaSyaYvO1zI++WtEDx22z9CXIh9v3kF5qyZIuO
+e4iI1dn2SN+uxzQPs3IPcamt7eKsp7HQYwwkh3TXPy1xWlF6MRvSkU+q20sgfVLQ
+pyHr5/Txb3EvcFnD1esZf0xFjwr5XcQFVSu625zGCKn2/0Z7BxtWaC7D1g3t2Jxw
+5CdS3C7C11daouWHzVyJmdUc8WrbKFDy3/IZHgYWJ3jtNJgg
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcjAcBgoqhkiG9w0BDAEDMA4ECLJqtxV8p51lAgIIAASCAVCiXfhPwBzmosBy
+p2uscyFJ3dTrGHFt/PDyjWfjj2KG1IwAPti/jUqRJkckaee1sims2AIfxfga7xlv
+wRgyNk6cNCHqjb5Khinog3O2N5FoJwLXnzmoPA+ilmTXcuv9i95mSYEzwR13kM8C
+9CoUXQJCvWnuVAw6b/InGjCj5tic1MDWm6hufgydr9C9njhP9Qh2NkN8/iseiiWB
+5preXZsXD9dyvQPH5qBu1aCnjLKYp5+bUl7UZ85mElvXOln5YW38DT1fX6XsT6b7
+kn3wR5ol6PabdEOI7KNJh8T1PEIImrfo7T2rWmkPC3Gqmv6LeN1kbipy61QXqE69
+rTp+dy/pCdtUIx9mC6lTwLf3Jy4tzOWObwHgTs1ikKvrI/di0vyd9e1xEY++Yxu/
+ZJ16Ykpb9Z2H8xmRk3OecCVbSCcZ1wLzOhwdUbsyX71GSOnDA7Q=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcjAcBgoqhkiG9w0BDAEEMA4ECAhz3Oo2dg4wAgIIAASCAVDYuvgJqqfDBcM5
+guRg1jkoLUvVq8tmvGpWjbQMN4Z7JFnbeZcI7QQ4IqflLY83kHlE/G+418z9LYNV
+Xg/3HmvGjAQ47niW6hwDtcyKKk3P1iOi2ZTEAkt//Dvo0W9BT9spqsubEbvJRSw3
+B4Q+Yi/hRRSsXhL/uSfVR4D6whGls08ryJr2cpQyZEOLXbGfZseSSt2l3T29mMas
+GWrG1aV86xU+P/fY7vLfKGUOtIrFD2WXWJTsJJaQZvCX/E7YmvcpGblq6CDnraFu
+DHTOtENK4Bx6ug1a+FIVPQGgjFDLZOkxLiVJUds88ATiaAuPwrUnPNUFNC6CX38P
+J8Rre1WNBKO7l8KVzIybUKG5+XNJQvmj0gGgf6Fm/736Y0rzJeiyBf8kSn9Z5QTv
+JczY6PbNppZLFuFTrZ772FtJ2oq6RTP1td7SmArW/YUCXxRaCBc=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcjAcBgoqhkiG9w0BDAEFMA4ECFQ5zMHU0moJAgIIAASCAVCJFnOaptrH+ouw
+C9BvIjdOhy+eW5Ic3pZa8c8PxXruW5ehO83+nWgnPzXisMPW094Varoqe43mz7Sy
+PpHpWfNDqPq61cCci1Daxm1d0PHCTKhyW7leowGT23TIM19Xz7FlMR/oiSZkHqzH
+SNvOGPaajylKS9Tmle36MSGT0Qla/k74ILf2HWTouwZWi5mhTY6DpLziPZGQBly4
+VMrWkBi8zYfYNfYtFWxTudHD7UmnLXh3nc71XEWt+1rwVJSE2TRur+GyV44g0Zwr
+9EULlMGZWjprXhU7oqevMZgSqXsjwB2vs9r9473vLfAQfIu4hKfYF5+WBwAOzPb1
+eqGNkrHMKEgUVMiTs5OrvWOVs2MUWKYufOdc4legc0Jd1WjfQSVSrQnAkvaNMCf7
+kS7n6Ogc5DrzL38k2IB82VhdF9pZy1uAJvWaNRqOQMIRyZAkXyk=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcjAcBgoqhkiG9w0BDAEGMA4ECM+XyPN3/xqgAgIIAASCAVAUYVbumKCpJl/k
+VhTOWoQX7LfUcfxoZTqfacml/itKo6GuNPjrtcPh1su6f55USVaQUiibkAx9LZbJ
+KpWtvMhSlQqumh8sJZ5XKOZsM4Nhuqp3ot8dd2VL0a7M6FlYFWf+hjU3Pr+WisoT
+0uPdYz/PbUwluswR2E0fAIMA13VgcKY6hroNDKVC5PlWXJxsrm+5GqiWlDmntaHw
+TDiB1sjT3BmSvcImTf5BGkvP2+i5+VN1plmKSKlyF6vdBa1VjW6ghEr8kCn4cBjA
+TqcemSMfEaZ/lbqpB3klIcS1chxT8sEdFz/9sXBFI4cl61Yj7ZJ3IwvbzWLqELQ5
+3c2TSYDxU+hrNau9VZkWwb7ZRALjHRMSk8MrKU35QalfSLl3DCEpNgF8fItmSVBB
+Vj0UfYskjTzHE5oWtTR0F3gB8G0R/I5Tru2c5Gn2ikbrRbC6gM4=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/dsa-encrypted-pbkdf2.pem b/tests/files/dsa-encrypted-pbkdf2.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-encrypted-pbkdf2.pem
@@ -0,0 +1,79 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBoTBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIcHjNGCgVdKECAggA
+MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECLRvOz07zDcxBIIBUNoQ1qwaTP5N1d3g
+HVFC+XKnv43vfH+RhbJk6Rz3GVqTgNkRZ/giTXko0CDblPo/uQNnwkq3rvMMSssE
+gF2QvghHUEuPOY0PvRBgbrXXm/9ELC0H1B5lgbWNNuNZu7h8rpgxvxwLQbGi/PvX
+B2W0VBsCCOqs00uDP7BEWoVswanwfYNqqLZ2+q1/tk5RxtSGt0r6wvQkKP4s055h
+BllEqGU0/ZZbZ/af3bEbZszDPgrietyoL1SuUlPXAkgu9+geAWOnxvCTQf4lnvxo
+X6/RGyjmQDXprO9HvW1ttM2qNStqAkkwCuMgYjhZpnKMnswILEmfORIKnXD0pZ1E
+2XZnxKpTgjWzwHmP7/GqLBH40+SvbvbFf4v3fIXbl861mc3u/rFCXZLmvhm5WS3L
++kZZ0CgLHfvRjY40nBv0cTwZZwoQTJwbquZndb3yVhK0b+T/ew==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIInPsgUWW7g0CAggA
+MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECOFhwmqpMeP2BIIBUM2Q5Hd7I58d
+biaLO6o8eC1NIodvvkdwelWuJQFIIneaIH956tLangnmWoCc7Qr4s867J2+qkZMt
+E0tYJoRc0xl13hGHUvQqSX9Bmow/decn4/um9MY8Kkwu9aEwQFUHd3Eldn5ktw94
+eLAYIMb7JtQDV2T1QiSW1oCGZj7HPzg5ZSjd4WRL/X5XYgpd84SaSaWHPy+Q+Fba
+eq7U84jHUgyrzo1pify+QrJAdRiWrgBlMgkw2Vvl5RFzToKjepTuwjX3rd8VUFpL
+TuKnrimXUrGcKVoO5SwjMpGAl8vyEcwx1dMeqdRdVrx/S4g9zZZoJ/R2Qz/M5o/F
+9rdy1lSnMoBUUatW3uCPETMfenwFYBRkVyYDPgdhDmP1OzI8R34X6zurpVppy8dT
++SR5H6PHTrtG7r8utZjhGtXAL0Lr5xaN8k3faMfyNbCAABME8Ex+ig==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpTBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQInlrNCxahn94CAggA
+MAwGCCqGSIb3DQIJBQAwFQYJKoZIhvZ9B0IKBAjYjwYE1LBgjQSCAVC4RY1jocAq
+2tw/ZiNbXAndRXAGSXHTRVKsN8ampNA9jptqvrWWK73HtRFb5nRwAF+eqfQJD+v0
+h7pIXCoKoErJiTj23PPQc69pQDbeH1rKLCabR16lnR20L7wTz9m5ZXonq8BAOCNZ
+5fpKk7sTK8aBRwDB61rys43PwdRaczqP49ZxnD04r5oztivMwcOHZ8k+vjWZVntf
+GiCWUgK1nfkIRNV2TpgzFaCzphxIOaQvI0oSWj4RDRSBp9QOYUalBkqhJrt7jSUz
+2Egdxfh3v/vjEjDQ3mbnftGamO/8s5iQUWj0tlp/2e2a9XRDMWi1s0j0dN/z2Lel
+oqZEcgLbl1ejKH0+3UHpCTYL0rH9YnsT99gdyszljtztg91LVUMc4qmCLE8Yb/sq
+bWCuxqSqZqRCVkR7EFWKkzIoMLZraYMc1Zeqt1BWZUqLCU8oU8LyMDQ=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrzBZBgkqhkiG9w0BBQ0wTDApBgkqhkiG9w0BBQwwHAQImzQMD2ZUPQ4CAggA
+MAwGCCqGSIb3DQIJBQAwHwYLKoMIjJpLPQEBAQIEEOjOVi0F+1g9Q9BzhIIUwdEE
+ggFQoNn6wN2rFabu2/HURiSNBVIMgWahYfWBTU1N9rKOg3QtI3hwlVJhi7a5UlPo
+RyLL4oSGpZz+JBgAmQEqr/PlcNiv3zvxbMrbZ7TcRzE4DwitbXRUXg6j4BhLs4ax
+wJRRGi0r8ZTl8/GGpOUJZj7VyLQmixNuJkkEXHpLuvXtobrecxb/g2ZWrMQx3goM
+bfAJbV49GrlszenHrd7IQ/Jtb5fG6qihri4St3j+hs2lkNHwVEfZR48i4cT0HDJM
+U8MeMjvFSpura3clcBNT6jhx/+cgoVfHZIw7xQl+GLUb0WTSID0qhq6ud3TH+qcM
+KpclX29qC4TOzVbVRCRA8NRYmmcaImgFdbcfiuvw0SKthcnx6dUmRktvX1lPmjdE
+0uWYTJdrL7CxKmua6QgnGnYNhNmy8da70e1IeikY7u76/6z5edLIxKKm4/sImexL
+3gfu
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrDBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQIm7wxCHZluCYCAggA
+AgEQMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBOgQIsHuXH5aupi0EggFQ
+v/LaahBu1HMmBk+wnORTSL4AoVrPHBt+U3Af/9JXK/BLu18iDesLvAr3wGmBIe1l
+Ld6ilGag2faE33rDKzQv6EMpR3UypA3ACoDsqscakFfa8JKCLbeD8mgFOH3vVUuI
+PO/Vx6hSCAvW20FI1VFtzi+/tVgkneuHML1k9IFkbKNzWx3NZQKBMo4kuzmO+WGG
+iFWwuq29V/9UiyExm5DnA0FrctYbchtH1lwPb6aHPineEujYzuld1963N52pjtss
+es9oc/KGRhQtAjHVmH0gGWnQJR7n9i4TelBIDpBC3VPrUBccm2W2hA2HIH3WYRxW
+WsP/Nkg3UveiAE6/nXYvI1KZBIzTGeyvB9tnNcbT8nZ9GM4zkbtoO5Gva66AEzv7
+6HC2zsOmF0fq0akz3cd5sMoRxg8JSMDPgYgsdJp6UYSQVI7xevFhCEBEy+0HVJLM
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrTBXBgkqhkiG9w0BBQ0wSjAsBgkqhkiG9w0BBQwwHwQIupwrj1RegoQCAggA
+AgEFMAwGCCqGSIb3DQIJBQAwGgYIKoZIhvcNAwIwDgICAKAECAkkdbW6dpd0BIIB
+UJtbsPclMM59KZA5Y7YbF0Ys5d5ZSWnsJhQjWKL61FXgIBylub/sqlJCB3f+mSxC
+GqHZjeKPJMlI2/fwqYY8rrcQu8ex6AvRN/eJhjugcT3bJUs5o3UIwljvdZ0xCeJD
+qlPTjV0f1EAaGmIuLLGseIPZcmKrhzu7+L3+ti5+G/UeGU/AqE7glyIgYrlKfJF8
+MMLdQ5L0K60cXBLO1PUAi4AYxxfclDdaASUcyE432CBxlGZN0yRO0zWM6BZ7uILd
+v8eZVYDJc6acWLRs2WtzsEZ4oxT0hAIArfAou7okdMbLBbfsFpAfbbyvqp/z06CT
+ZLlGySZPBWQc37d9FCSz4a9+6wCXqME49uozxxaFSkLQMWcxeTefrWyrAVD4QgIP
+g689TrBRAnAtdBIX84aM0sB4+rHJxr9hF694GKPm7Wz80OwVY3+3gYHKwzLLBbRu
+ng==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBrDBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQIdH7jrafsUl0CAggA
+AgEIMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBeAQI9jyutvOWsgoEggFQ
+sf8QI6BdcCfJmNklZu5NRLmK0GyU17V++XUaO0yPt92xlSC6h3bEPrPz3GxZrnOb
+ViLubKJLUqQUb+xZD9fHq0XRpRMnRSu906RAXR8FVreKwgz5071G5KsgJWwoOQhn
++/6B3A5NjZbjwGkd971fGbOcOljLGqMfTv8KSlZoGTeKDLOV3Hk4H8QNY6BfU307
+Sr39ETAozWoVVoQYCc0VmfA9lZEny8cvwRS46X0qj2HHUACn2qEI+8cOZkOUnv4r
+xL+/gJ474TjPX+Jd+KIndtWHyMPjRY6pRgdmsdWxjQqD+vEWR7sj96w+/x56M+mb
+hH3TQua0V7iAsoLzdKyob1+2naI7pqAaqxuRzVN7mATj+MN6t0NGMctiVy5LDb9C
+yJXUwLWN/8pgn2et7fbdjz6mWaUOgL6TIlG+OcPlp+sQUHwe+2rs88qAHJBjPYQW
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/dsa-encrypted-scrypt.pem b/tests/files/dsa-encrypted-scrypt.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-encrypted-scrypt.pem
@@ -0,0 +1,33 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpTBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQINN5s8FaPzoICAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBAgQQy5+msvCgFnxMEaD174O1ugSCAVDyvwNA1u9r
+Nj3DaCrlz0sE1fPAJJUCylvQLt6WGUvdsSAliFjv+K5GhtbNRbCUzXtCbWjkqMCY
+rwCXfp8CCvHFTTR8yMsuvyThrCfhRf9H1tMJaUDlAP9iVHcydTKeCgrIpkxNNNFB
+DEhQr6M1x7cOLCKsPB75Ap7v9Ks63p8UwhrilBjdIFnHJEEbzZsoFnY/TzMSTG3y
+pn9/cNhY1KVLWRr8j+6gQ/zFjAwqMyrI0ISoXim8rYkHEeJZGUXmYs0orLrxoqxe
+uSBkbr74rtHDdDaxv2ITFI5V210SY73mc/0C3DCy6ibp0flz16BsKd+EbOAExRKE
+x7zJSxJ1/gQiuP+FFKSN8s+cm5Xn2FdQE8D3h8csQ2HrTv5DBLdDL7xegq126WPE
+e1HlZPzI9F4N6EqbUGXsoYYFMeSQOMhVP2C+JRWYolVTtLJqAv0i8Ao=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpTBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQI/FyWk+QpeTgCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBFgQQHSr6nDgm5G3mfzfwG0zeyQSCAVBZoK70Mv5l
+Bg3qHpI18jLTCgLqLu1cc3PwugzEqNa1ZjHSx/Kclqz4rtWar7omc+n403RGYsr3
+uNnR5ENXabA7yCj4uQ492dKlFR2Qr8+I47PgMvjRpmqkczCCCe2JEixc3eRqzOK7
+hTjAt03SW3OgZWL2VeZM+cF1CI2TphCfEt839APgXnH53BOMsuOGS+7gi80Z3HYV
+asxYTqOCW9b+CdQ/Jozt0s3LmuLElUdvRHBhvKtb8NzIePrCDhZSGGSGS43Px2JR
+c4yHUC37zNVWW24EvAe4WadVad2F9RS/1Titgxmu80Xd+rERxh7TbbRWecYjLQim
+T1XFmmqNHqScu8kypMEf0mElZx2M38uxn0VJy8wmkgoK4KbJuNvHRAOM+bcQwsJl
+GGxvH1HAt41oJ0FTSr71UV/nAD/tYJ7XuZEtZKYqKfQSN+JWYxdvkVk=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpTBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIToKQdJwvmYYCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBKgQQu9CT+JV6cKk50hxDchfwRQSCAVAwblK0RQIQ
+1mRsFPmETlN0BpPuUhqmyPIR8plot/YYag2YomVMnS2fRpaBILtqv6yrJJq2VTgx
+TI4lsH9vsIrsDUjVc8Lzo+b/Bn3+90i7dKt1Wl1bOSTTyDt/FcAU1pUP/xBoaDTu
+6in2XAvY5C2q2VVhvuJqhRkqxjYNCP+aof8qVijM2sw3qs49PwRw3H+V8SQ90GvK
+ZClddQdW0Chl9SH3aH7SKFyOfhiynI4MOauc1L19S6CXg6QfEOmi/EA5XVlzmkgg
+Ugpal8y7LE7q0Ua33Bxz/LDR6izRj/zaIMPXtahmQOzc7F8f9qTuyCVX56hl/KbW
+jerauH8Ls35UM60qtqknwIH9aN5rHU5fpP0i24qS/DscIDQRIJHzux7XfQO3EJlK
+QdowSSijvlrX2mz31rIuD4kmBsgdCn+x4AOimS6BSqMWswo5W/Mppds=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/dsa-params.pem b/tests/files/dsa-params.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-params.pem
@@ -0,0 +1,9 @@
+-----BEGIN DSA PARAMETERS-----
+MIIBHgKBgQC7Wk5JfPvqyloNxI6wTHy/MAH7ELUs2mlcjF4/+ZAeKbsAOV/m408B
+q8+3UrLuSQkdt3GP2xZdQb4G+ALUJI2gSQ7RvgDY5ORh1+AKOTwJKM0rguCwtlw0
+Nb4wxrgbsCigYDFMv5Zu8zEwqZaXL14M4N1VU3YztLZQIo+UiSgONwIVANylBkcI
++baMiQNow1/dYmCB7NDHAoGAMouAkCn9WUZhh+VR2cnNwBZLTi+D4VBpEhCGhyvN
+pQlD9l88Ji2MKjO3eAv0o3AWCFcGSWjTzaxDCKfnY2CiEHZ8Eog6y8E9fVVTyhea
+1Di8g3UvqSY9mR5m8SQ4l1WiMcEl6FTUIOFQNP48QfeOjLazc30ALVITVSmVsdUG
+wTk=
+-----END DSA PARAMETERS-----
diff --git a/tests/files/dsa-public.pem b/tests/files/dsa-public.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-public.pem
@@ -0,0 +1,12 @@
+-----BEGIN PUBLIC KEY-----
+MIIBtjCCASsGByqGSM44BAEwggEeAoGBALtaTkl8++rKWg3EjrBMfL8wAfsQtSza
+aVyMXj/5kB4puwA5X+bjTwGrz7dSsu5JCR23cY/bFl1Bvgb4AtQkjaBJDtG+ANjk
+5GHX4Ao5PAkozSuC4LC2XDQ1vjDGuBuwKKBgMUy/lm7zMTCplpcvXgzg3VVTdjO0
+tlAij5SJKA43AhUA3KUGRwj5toyJA2jDX91iYIHs0McCgYAyi4CQKf1ZRmGH5VHZ
+yc3AFktOL4PhUGkSEIaHK82lCUP2XzwmLYwqM7d4C/SjcBYIVwZJaNPNrEMIp+dj
+YKIQdnwSiDrLwT19VVPKF5rUOLyDdS+pJj2ZHmbxJDiXVaIxwSXoVNQg4VA0/jxB
+946MtrNzfQAtUhNVKZWx1QbBOQOBhAACgYB/y1sYudWGbLsw4iJF3ajwAfo4ian3
+HLpmKRuJvpyGwmgQwjF8cSpn6/7kAXGBSG3QFOGyuFi+46B4wo13gPww884w98Kh
+Uimse8RSmwZgCtHgtFHcFZZ8fanifWKXbpOfPggVqkJMhSQiJX9GiRZShMvARL7j
+wuh5btp03PUh3g==
+-----END PUBLIC KEY-----
diff --git a/tests/files/dsa-self-signed-cert.pem b/tests/files/dsa-self-signed-cert.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-self-signed-cert.pem
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC0jCCAo+gAwIBAgIJAIRvDyrolgpNMAsGCWCGSAFlAwQDAjAhMR8wHQYJKoZI
+hvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTE4MDcwMTA5MzUxNFoXDTE4MDcz
+MTA5MzUxNFowITEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTCCAbYw
+ggErBgcqhkjOOAQBMIIBHgKBgQC7Wk5JfPvqyloNxI6wTHy/MAH7ELUs2mlcjF4/
++ZAeKbsAOV/m408Bq8+3UrLuSQkdt3GP2xZdQb4G+ALUJI2gSQ7RvgDY5ORh1+AK
+OTwJKM0rguCwtlw0Nb4wxrgbsCigYDFMv5Zu8zEwqZaXL14M4N1VU3YztLZQIo+U
+iSgONwIVANylBkcI+baMiQNow1/dYmCB7NDHAoGAMouAkCn9WUZhh+VR2cnNwBZL
+Ti+D4VBpEhCGhyvNpQlD9l88Ji2MKjO3eAv0o3AWCFcGSWjTzaxDCKfnY2CiEHZ8
+Eog6y8E9fVVTyhea1Di8g3UvqSY9mR5m8SQ4l1WiMcEl6FTUIOFQNP48QfeOjLaz
+c30ALVITVSmVsdUGwTkDgYQAAoGAf8tbGLnVhmy7MOIiRd2o8AH6OImp9xy6Zikb
+ib6chsJoEMIxfHEqZ+v+5AFxgUht0BThsrhYvuOgeMKNd4D8MPPOMPfCoVIprHvE
+UpsGYArR4LRR3BWWfH2p4n1il26Tnz4IFapCTIUkIiV/RokWUoTLwES+48LoeW7a
+dNz1Id6jUDBOMB0GA1UdDgQWBBRMA7NzBEkpRRlo/9Y7os6U4sJjATAfBgNVHSME
+GDAWgBRMA7NzBEkpRRlo/9Y7os6U4sJjATAMBgNVHRMEBTADAQH/MAsGCWCGSAFl
+AwQDAgMwADAtAhUAoLfr7SzK6rBM8F3swsns0ElLXRcCFBEoZvR3RDx/X1Q4CvlE
+6nXQdLF1
+-----END CERTIFICATE-----
diff --git a/tests/files/dsa-unencrypted-pkcs8.pem b/tests/files/dsa-unencrypted-pkcs8.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-unencrypted-pkcs8.pem
@@ -0,0 +1,9 @@
+-----BEGIN PRIVATE KEY-----
+MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBALtaTkl8++rKWg3EjrBMfL8wAfsQ
+tSzaaVyMXj/5kB4puwA5X+bjTwGrz7dSsu5JCR23cY/bFl1Bvgb4AtQkjaBJDtG+
+ANjk5GHX4Ao5PAkozSuC4LC2XDQ1vjDGuBuwKKBgMUy/lm7zMTCplpcvXgzg3VVT
+djO0tlAij5SJKA43AhUA3KUGRwj5toyJA2jDX91iYIHs0McCgYAyi4CQKf1ZRmGH
+5VHZyc3AFktOL4PhUGkSEIaHK82lCUP2XzwmLYwqM7d4C/SjcBYIVwZJaNPNrEMI
+p+djYKIQdnwSiDrLwT19VVPKF5rUOLyDdS+pJj2ZHmbxJDiXVaIxwSXoVNQg4VA0
+/jxB946MtrNzfQAtUhNVKZWx1QbBOQQWAhRhml7zu5ShPwMQujf9s3NuLE49pQ==
+-----END PRIVATE KEY-----
diff --git a/tests/files/dsa-unencrypted-trad.pem b/tests/files/dsa-unencrypted-trad.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/dsa-unencrypted-trad.pem
@@ -0,0 +1,12 @@
+-----BEGIN DSA PRIVATE KEY-----
+MIIBugIBAAKBgQC7Wk5JfPvqyloNxI6wTHy/MAH7ELUs2mlcjF4/+ZAeKbsAOV/m
+408Bq8+3UrLuSQkdt3GP2xZdQb4G+ALUJI2gSQ7RvgDY5ORh1+AKOTwJKM0rguCw
+tlw0Nb4wxrgbsCigYDFMv5Zu8zEwqZaXL14M4N1VU3YztLZQIo+UiSgONwIVANyl
+BkcI+baMiQNow1/dYmCB7NDHAoGAMouAkCn9WUZhh+VR2cnNwBZLTi+D4VBpEhCG
+hyvNpQlD9l88Ji2MKjO3eAv0o3AWCFcGSWjTzaxDCKfnY2CiEHZ8Eog6y8E9fVVT
+yhea1Di8g3UvqSY9mR5m8SQ4l1WiMcEl6FTUIOFQNP48QfeOjLazc30ALVITVSmV
+sdUGwTkCgYB/y1sYudWGbLsw4iJF3ajwAfo4ian3HLpmKRuJvpyGwmgQwjF8cSpn
+6/7kAXGBSG3QFOGyuFi+46B4wo13gPww884w98KhUimse8RSmwZgCtHgtFHcFZZ8
+fanifWKXbpOfPggVqkJMhSQiJX9GiRZShMvARL7jwuh5btp03PUh3gIUYZpe87uU
+oT8DELo3/bNzbixOPaU=
+-----END DSA PRIVATE KEY-----
diff --git a/tests/files/ecdsa-epc-encrypted-pbes1.pem b/tests/files/ecdsa-epc-encrypted-pbes1.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-encrypted-pbes1.pem
@@ -0,0 +1,88 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBoTAbBgkqhkiG9w0BBQMwDgQI5MTTa4Q3G9wCAggABIIBgPkvqzm83Jam+28T
+hCMbmBSmEJBexnqiWndSP9z/RJZ4MVQhj98K1hs3PnwAf14PxYhLtZ1hff678KBr
+/DQjE6jTErbZ+D1c2jAF2ZulG3PsjBsF0FIQw2Bf8P/RnzVUoqPoHpYW9BJZhcG6
+jN4OhkzKX7XqSUBE8V+tbhwXGnTR7jbknQ3FGRrHSNiRTKaQn+EWIwsyg0LJgWGJ
+QXVN/fetsUgFeQUj3CMMd+BqmY7PE1E0v0zJ4A9yj1GL7QGilJ212k9almsyWwYA
+iNrTK4Ngo4BBFXQUmoAjmdBA2sxTyOwmL3FYN9ju2E8IAQg26l2dozj6VPOCQv2D
+/biVPYinmNQPrpq4psv2wZgo211XrMqPRFb11coz7OGZXUmmaeEzsc30lp4N4fa4
+S3HbIvluwgTan68EANSZcYR/ry0MGoQjsbb2FoKMnuEpTXQLEAC2b9PYKv2+Bqg1
+bsPJPRBFzPxiDTjyhkX05mY/8r20bfBoXERh2ENkNZMEkHhvgQ==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBoTAbBgkqhkiG9w0BBQowDgQIpbYR4hJkTcgCAggABIIBgMmPK83NuhSsNiqa
+jgw6o4vWyngy40F6wBiK0/ouAb9e0tCGK7/TCPq49qn4u5Q7ble/Ufl9p9nbL7+J
+HnQ0uiYq8aNYNIB1VtbJCrwBRz7lG05nwvbydT3uzsEIbgHCtHg+GpP2sPuipKxZ
+k8AYpOlT1etn0am6masjvdBWQlAdq534xfBHDCDwrcYbNASe20oQ6J5M7uJIKXza
+FVBJL7pvWAHuZbB9tIz2iz+wHrQKXdmhmJ/ePfoOnXWd8WGSgbSCLvgd0iRgQ9Yc
+4mQOE7ZTv4koM2PtpH3qGpOW7hgFDEoWXouolQpXtGiQeBBTBWJHBDy2GSYgPUi3
+Hj7ATyoCiP2j3iEFtnyhs+6okyOBhWjpKfNPQoiU9MhgJyna2n2jh3TU7c+cl1z8
+eIo/5pX73alFLsyu2DSoJdt3gIyroqQHJ4n+eKSq1B6/CQLN1n2zUk1l2umnC/1X
+8qQQMvOaE+Wgrv4puy9Nuh2BkwuXAirZJ2UcYwBrel5suC+jXA==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBnzAcBgoqhkiG9w0BDAEBMA4ECB2jmfJAz7EfAgIIAASCAX1FgUt+XM9DG4Ay
+2yD/npOTZp1izQkniOT/3b1XqL0KMAZuYL92daHAgBM7hPO7sZGWh+FQBkX4eLNG
+Ak14idq9U2TCZmV0lIDKWR897DkxM9LW2YwAQxXzwH9frqo+OyUQNBero+F2Znc2
+kEpjT85W68qpGI6XFXFRUTmZ+K+YEtGDtwvm/a5hV8CdGAg1NrkJEu9g1NeUEEg6
+4PRLRv8IgPeNEIWJBhpw6WXgIjkhnf3GHGYEs9RqO041ZlRHE7LeRUY9T1NqcQ+s
+XEm0KM0NU5STJthfaqsv5TK2GUHgHBPex0Hwlht08b2mp2YZokBfxhgucUgWSeTg
+OntU6rmJtuOy8bvisyaR/G1em/F0L0toFeetcq16U3EG/ZrbJ6mW3tgaVrQwpoFg
+6fwJbgi1rTGT5Mel6nphcMBMK+/9AOw7MO8pkn97ddImFc2tqK2e2L8GYjpSLEiG
+0dK0Lk0m+kb1iiwwQ+KbgPbLBG+31jBHANL3jlewXd9oKFc=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBnzAcBgoqhkiG9w0BDAECMA4ECIulPRng61PVAgIIAASCAX2wES4a4zOo45Qz
+6Lao4h6qkdtZeiJtNCWkmGoNsNb0GQi+HpOD71h/E2HLdpLxAV1RNbGeFV32j9Um
+m1iLRL/S+wM5/CorkV8lSqZd566kXUWo8dYU1aSOBwpktB1MAXBUaPvlM3NZQyVN
+Oj9ZTTV2/AOG2Ca5L1rX0nG5A69m50h0EHoH5MV5cKIoTHRfnOx4jUqSsfQgRI4X
+wo4esYVraC3L6Onzveb4Q7H5DCF+4AY/ziNRQRP91tvOseti291qZitGq6DY2t7s
+QOl0oLFU/obeG87RC4syLQRm/xLt6aXIAvHzy+V5h6QwPnZAtVpLyBAYLbVx06YX
+1aV/nFw+F7U+9F9daujM9upr0/c/44JPwhTHUCMdPMkmXoF8RaSdQN0V4UHDMBnT
+t8ZXhjMxC/B5aZjDvyEnMtkKTllA7DTE9gU7SBALt7KaGX/dM7EpZbVs26VESwiL
+hT/IZxtZ8zjwMkF+ixOOOZR0eNf16TT+kYgU9k/yYPzYLuc=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBojAcBgoqhkiG9w0BDAEDMA4ECGcyMGdLvxlfAgIIAASCAYDFC/PBeI3Yl8m6
+QQjW1KMrAralt6XHcZMncSBUc8Lm/Kq1AHntjXMqbph0IKVLiqsTJ6DfABXNnmuj
+vCDwF6VNGw1oJSAFXy61PKmBIQY305kDOvqjou+oxTdczun2wfAsPjLWksQ+nj4u
+4VRsyMyuw+W3B9zqbDgc6vk0J8ps6VYQQsdngOjRHAeF6Xy89GHJ7jKEA8hXlizr
+7QD7GnD6gc1of3qMFgZyTPOhvgLUuGHTSlrTYd4mo7bzyVMUWq1NHtz2TVeg1nUe
+KZLKgUNryL5oe5uw3gbflvkNSQOsbJsmp0QiM45Ont20QmWqtGtkig7pYDhltff7
+VXomwNhCWAsyJl1EOrDQoegUGe9Yj2C4J7aL2SHQ5+molUAZUJfXo1DjyxF9E4BX
+kiAL2Jx5pQGmJgJhDoJOLc0Q2ZsHJdpHPgx4Ajzx68FzISi526+vdndLy0woULbM
+nuaBujaklBpqBR2KvvIYjkYURKE1mm03C4h9WEryPqLvHkhvPKk=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBojAcBgoqhkiG9w0BDAEEMA4ECDZitMOHFzavAgIIAASCAYDpF4pB0sPDmOKG
+/w8AWQ8/uq8F5tzhLw8xXOwfj6JIN9K+p6VJMkRVNMeLRWNW2d2AdgWRhhZuNpgO
+N0Jb5TfVVYOKWmuC+hfp8eyS1TY3ortUnXjEqQqKkzDxSzwEKkjTLyHGm8+h4g4O
+STWJtOW8mZkILSe18syFw4XuTpzcTmkXpirV+L9ggDR1zkl8KlrpTp8/9XGsUTWz
+Ifl/5O3fhqdTse3khgAZaYvPeWnsdGZVr/KAO8iSannaMC0yzwl5M2y4WtJ3sMgw
+bNMkLATiZScQgo1AQnelLIusxN9XZZPFhpjow13t9+02pG6jpvl58Apu0og75Y1i
+NQDProzjDNces9XLBEPUvzXFfcGW9DMrthabuu0b/zRTj1L0McApp9r+pKTVgkng
+e6QdHpMWBV7A0lmpXqwr3AzXGhRky5UNNLQTMZbP8jhTXkADsNgMJgwmrNyW4MzL
+EZ4kwPsPYlv+vLjeVsiqh4KOVqSC/khv3e0NOzMredePo2Of2GE=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBojAcBgoqhkiG9w0BDAEFMA4ECLnZQJKaSTr8AgIIAASCAYC6rYNwcVH3WG21
+aEmBR1PrTqtstosJzDR6KxNLxu+tQdKBWxPGXJqhGViYzQw/3S6Us5BxisZBQrHc
+TS0B3Da2EvSwVPj05qRWRVGmmpM2ODZwQIqOGmMHUMroiGC4A1W5wcY5CFZxUT2W
+1Jfif0uxQJm7bVN+ULJEe32z4hSPj17p7JMbtQ0JbSvoJ+oIxog+EpjqH3/OWMJG
+UdxkyZlfpBx9l8EmNctGoCnz8ZU7BPXtBshC6zMwygaN1vAF965YtX4+qTpwwcxX
+yhANICx/3OYxcKUwmO6go0SaVDzkvuuKyx9BcLPwk9BaFklrdwcwQsv8FO80o3vk
+FUtk/anXILg5tMj74xIaUL8w3h9rg8K5G3TXntOyRq53bAO+GF0mUQVSB6hTE1J2
+9hJFDUkvbsW0EixgWI64FKkorZkPyznP6js76GCuDzM9sHN8ywWsnmeK9kuGyDor
+a/2AuFuVa4rDZQ8aDymEdD/bL/1f9WYViblqzkgBnmTdXzv8RUM=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBojAcBgoqhkiG9w0BDAEGMA4ECK7Z49fQfS7TAgIIAASCAYBSEjWSOqI25pHK
+KmoozeWVJ4+WOwjxEXjuuDkEHOzbEaGrtYF9z66EBurZ9rJujpxfgMRtt8P2WyMW
+eK5JXHvlgoE1PFGjqmvuTSOmUp4ZUxMrMh9gFb3KaaaSq/AOQj2sZmGC+OqE8U9L
+yLO7HGyOtIc1UnzegINaC9TNj1/D4JTy1XgYm1S0xcgoxKZKIkhwHpQYRwByvXWM
++wS+fdBp3a4UEKxUlgHsA6H35Lmu7epYlQqvywCj16Mpzkv7jquc1TLY+izuiCee
+d8yKGX30DN+lh4svbmD2oZPuB0V26wMzhOCx2x0WbJibBVI+NBqipy8BH11w7KIt
+hVTArXB9Unx0y97m7ut8LBqD7ogxqQxXzuIFYibMh6cm8d9F1KSZzEjHCQ9tZL93
+eNKenZIFoLOkCcuD4RJfejW8zzPs6ZKKXpF2vW2ahqkbZfqTaRCu1iq9VETSMzam
+WannQc1uCzk1iTzTUrsWZ9ULtGeU7Yp3fvW7dpQB4AtAqi+wBPY=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-epc-encrypted-pbkdf2.pem b/tests/files/ecdsa-epc-encrypted-pbkdf2.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-encrypted-pbkdf2.pem
@@ -0,0 +1,86 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB0TBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIaXF1FSWH8FgCAggA
+MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECEkew8xuyitxBIIBgAdLhNAUj7Bpglrd
+cbXUprOLFwtnPu/WJ5VQw5JuZPoIxxyq55g2jmtE1rK+25YZET9Pzyf8L7fkG1dh
+Qhi27AMcefNvbpRncwKQQYwU+C4kJ9PV6V/dvCqkfNB5ea0pbozI9zZ+ts1n7JpW
+szP9N8d51I7n/HIdpgPzxiV0eD0ZLjkUoxoRIi7EOVqXGpOt4mGkOW/mRgTD8wv7
+koZSWM1hTw2h1hBvPPdWQih9i3ixx0kF5l4ZASjibnKWJZ+WW3C4kTK6xYGQWwZy
+fdOYiv0jFClRsPqHKtIr7Eg8TOtQb73rny9hQghchLWbTRuZDzljlV3QA6Hn4Tz0
+pM7GfCrJHISkiWj8VXtKm6xSDKF6kyPUFN6MCd35IALnuRdTTGFua12nJye+R7lV
+lc9RJOlI2503csKTQK2uGRKRMt2m7uaRqNy8+B9+s0btN08oxrs7/Ui51xcUZ+QZ
+t1fDmyH9ym3KPeLBui39mDGXvyjNM3kKcWOFwEXdCYURHWMqVA==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB1DBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI/fA7dpDtXHgCAggA
+MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECBsyIBlhZP2PBIIBgLBTH21BBWz6
+XWVouzSpVJ+XkekRTPrcK3pNBa/1sGCej6CTWr59cdcOr99DZ5RagHp28Udee/n4
+WeOsyb0dfbUL9hjgM+rzpo9yNndwVvuRhdy7rpYDqcYkhSo+2bLIEgsItMnYcwbP
+x6XsLrv+ffxZlVIlRlhZUf6bGhgsI0gg3AIXnmTYQ1aX5OjZYoNgGUoOauL2d+xT
+n6z3IVfHlc5wFq10U73J1arv2vwsfM8PW0twnPeEjRK2WR3JQ+fKBoz+irESQuQq
+PFjKcWGTw7oBYpO4Hi3uKB25GzFNWLh9W5dqQ3gt+L9x/gbTlL6NWuG73CvH+cVX
+SkjFbncSbSbLA51HDJPEtipCsrZWKeWUK89xaKAvaIdsvi4rDJPI5CRCNpiWjdyy
+stkzjrhD43LLaX8UBwQzNXNm+dc/Ku1ZirWW6gn/0vZ2i6ioe618H2mAIqPmCFIb
+mgNIHxBhUwyYryiGHtxFDjjs7hK/EuinWi/RbFLlJb6EviF8cNPrvw==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB1TBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQIbZbOKhH2r3QCAggA
+MAwGCCqGSIb3DQIJBQAwFQYJKoZIhvZ9B0IKBAhhyYd5c9VWlASCAYCLhBDXCXyw
+dSdrU/7zCYpT2c9wlSV2xXQReDruSOO7Z6E3PVSf0mAREzhk97kXCPGSQMqZqy5n
+GmxcGT933gvGwIsKpTeCtsDwiuxxD/HrbaehRSpXGTKI1SaVeZpKtdbvc4MPcVMu
+26BI7EXRwDbmJbCJkg6oR1xSuEvbIewfq6Yy43vGrUK+pnSmaLg0bB7PnHuojb1/
+KGfMZBzB1eavCwWz7htrVj+e1D2jKr42aVDLCMt6ROqb32y4dNIpMI55iiE5V+Rd
+FsSfrqFkYpvzuo216S98d1/YUwZ4SG/ooZC8yzbZ+3ZChMswBzs+HeFRBBI94RNY
+t8cW4cgw1tbzhPfckcHg+Mruo/IKfL/9xDdd0XLQ4ArX91fOOu8O5SfoF69uVj76
+vLwao/z0HpqTVhsgbpseWol5k2Dlsmc43O8x1AdKE+Ua5eHgSbEweDzqSZOg4CwF
+CIt0wqx9EQgxWAfpTATv4pGu3/lG4POZwfauvHdb1fdIG6oiOhUyi7o=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB3zBZBgkqhkiG9w0BBQ0wTDApBgkqhkiG9w0BBQwwHAQI7J2iZ7SSLdwCAggA
+MAwGCCqGSIb3DQIJBQAwHwYLKoMIjJpLPQEBAQIEEO87ALh2iTMdF6gfPDTAr/0E
+ggGAIH2qibHW4uIFdj3hmTTLaWXnKDtZiv5xA9HOvTcKgEx+9sxqMkZcHaTpGBq/
+AgnktUJjmfNItwoABC5iWUWkhBFBtCxtiT3TcEdQpmvHDrzD8LuujScPRq+rcjIz
+u4+QhU0ZRKB6rKvfa6a3TkseH4QMRjwsVqt/vbBHhpqWBBpgc0qrK5iH6ioNgH9r
+sW0vemwY2iklPRgwpILAcFYx7GTfmgZ5ETqKvjXD/K4JZ1c9B+jL/e5Bq3oTXJbx
+mX4u47F0BcqXfax9Hg7YMuVtA633+zW+cTNjv3PrsnhPRcpLQ0JAMauh7bu1oLoI
+9taZ0mwOMt1SZv8J+vR3i4t6TQ1F/h73gtZB428islJKpRS3U6v1aPlLZcfbN9JR
+cN2FdArs0X2hK0zwN7uOJJZxSegONq1wctEsXB4E8T1q/r3ojFKuwvO2mwkrLRgh
+oDseVijnW2R1m3USUdmaaqxeykLe0CS3beybFss4QbX4F/U/x17z9dqCLzoblH5D
+Pb2c
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB3DBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQIC6ZTCRU+rCsCAggA
+AgEQMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBOgQIoFMVJ3EE/O4EggGA
+uA8nT1iW9fbvxFCXvy2myN9mwQyL8rnfLgbDhYZks+FyZm5tsDLbgw/d+ZhonzOM
++PcVpwmcbegT6APDWAZjXvru6gpEwfxFzBjr8uoaKFdf6wEKbF3lTauxsgJjZT5X
+uykw5OBGnV3yv2KBX/wumL2g4xiHKbMpOJ7sC4OTs7r6m+uMjimHKlNIbJJMUfq3
+V9afDbAz/epyoe96MqFGo0HyA4FsR5D9vKGqZFp3wMXpRFOSJIV3jGzaysVQkOXA
+yWRgUM0W1hL039hNteilCy4dMd7JqzFeksNz2wgVoLdUNu41Bvvv/NrAi+WG09Tt
+a82kRgJzB9zeVA6ZpYe+Xy8JpSjvXaVerTblODxVymBKT0cQofMRQjZsalLdWcy7
+4GrpcpHNYLJ+hvzNLqHeWUBo3P79Chl7FZmFNX9ihdenjb/doTKB/eH2je+x3w8x
+xmt7CfeCpcR8EJRDb0GRL7yAcnmtBr2JJa30h1WeR5VeZJZ5chaXYsPBqv5HNomr
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB3TBXBgkqhkiG9w0BBQ0wSjAsBgkqhkiG9w0BBQwwHwQI0g6+CtM4m9MCAggA
+AgEFMAwGCCqGSIb3DQIJBQAwGgYIKoZIhvcNAwIwDgICAKAECMcTOOizSU7oBIIB
+gJegyhG+XB4/LH/gr+OjDdC5aiht53g6pdhZ62fkmIf0zEI0ACO3K6zRhaF896HO
+KuQU85Nds3Oy4fMci4Q6XjZw2Sebxjw873HW8rHgXqlKanqixx1b9eDXJ+GeApqC
+oyHqxJeBDpWTo1T4SJjAIhITqn1AQQoKI/FWvjwMq4gD0536N7ln3O8omOoyV9i5
+byqgxNLLZ6Ucues8ElcRW4rza/wKHvCMmESs8zDXyW74ax6ZMPGZ0ns5HDR0mVuk
+dMZ8vqKQDDuanDcKh728iMIevqFvagKdQI3GpWdr0F4o9JgZqLOk7dcnw/1Ao7i2
+onBCk8+exCkyFY0zwXquzZEWongrOzKeyNoLdAYNe1ST/q/Kyr9pxxKpIPTEDU5Z
+8Xh9qd+pSjEumqIQAVC2a53FstPJOKHVPjR7NaUN+9b7FK4MrhtW2itsMdI4Dtj6
+BjxAqBzW0l8QaJHhr0JHqzvEPDZaiFC2jCjFRSwfOAvb7vOo//gMyJqqBIj3Dms5
+2g==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB3DBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQItKps3ADDI0MCAggA
+AgEIMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBeAQIhgAaNSOe6zcEggGA
+P/Namgvp1HsSCKYgggEIqjXcvE7vIcDcEs/YyN/dklLfn3QkngE7Rzx7zx10ZFMG
+E9OkfDzg3VWAJ6vzTAsrGb7h9sKRRtGXMxzM8eL6Umq5IADc5lvD0UYCPLBFGWp7
+XFkFEWmZa2o4/abfKFnQVI8C8XTvE/LDNWmCmwpO3TBs9c94fl+Ra16BIVXMq+Ko
+OPzsQUDyIFvoAGbnrJQzehN7r6mW7usaiJdcqTvbklBpzo99RrgawIT3A6MGfYFb
+TLiDkA4cliUNWuoaOU7MRjQB5n249K8cuvD5qcgpEXiK2SZsZMTuU/zmGWA2ps8U
+FKD+sKHi8iU053HDBFUKRYZ8detecUOjGABcJvJWfWDNhKX0KcvK0RXYgVXXaS7q
+wVOtxYoITE8GkEG8DlCvF2UjHD8GZ7/JeFYm59E56ge9X/+pl96J2gZftN4RLgQ2
+uecWdCDjKF0nJmFgstlHZ8VIVXplzzjIO4zyQ9J4QqpOENcXev6BLQaleR9vB/af
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-epc-encrypted-scrypt.pem b/tests/files/ecdsa-epc-encrypted-scrypt.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-encrypted-scrypt.pem
@@ -0,0 +1,36 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIJXpAnkCfm7ECAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBAgQQeUbkFh7HD9nug87uhKnZ0gSCAYAerhWsm8dI
+DSaF7A+iBX106H/qjICoREVHN/a56FhO5HwWBGdIgfqv4i1R/6Uv4eAXWog5pQUb
+MfRRHNFlejqMik04TPx4kzcsmbToePuPtSJiyDW30p/zlke/bTXYVQXyTVSgzqsX
+O2HgKyOj5HrTCzZqM1OKS6YYlXRIsafgjJLlZDC/FybF/R1s/0kCugHTLqEDpHXx
+A0gcmvhox7vCScAh4616ay9n+R9eDfVoHEPKbRvcXztB+uifJtA0D6MtdFk/CSYi
+PIHjKcP4z/qe46mjJ9PyU/eOCZR+0ppowMwLSKTzPl+acDq+asCiQNoqZWMNUSOB
+F2jykkH4fFo1cKXkufIg3LkN4R90oHAK/9/4Vo4SxO+imiNfiEc1ktjedLrF0DSe
+6zxFFe1KI45wMC5nBxQfDxiagYN29JZjbb8Up7gs37qTWpSEOeAxvTdTru+u6CaS
+zug8ow4MhEnimCy6q0cnq0xHiF719UiPW/xFRiZyTXTyp68JDSxoyz0=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIB7B9+qlAXBYCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBFgQQ/Sn/EiqSFcj4ea3YHieu1QSCAYD3at0cyQ0l
+gp9S9MqBUEkHef6drW8CPf/oTnzfEOomtp3mXtwU4trzImpELFGlORl+gNZ9KUoF
+YixD62N1v4MhE9Ff47pN9lx2IYiLuekJaHqypRnrh7qU3V2KsrcqMcyNUIMyssLo
+CODMNxfiqbBAsbVcKnfxq/KBenT1Y/AiaAq6iPGRxora1GccuCSurePV4DbWOkRt
+sz81u1yIX9kzn3xVj/O6IubkDet7uFeFSro6MdPM1TlRMRxavXuCpO7yGEHC/Y1x
+4Xq4J/wqrg25kqrntbkomjTgf02H2UoNhHIOgDZCDLTYJPCllcJOU5hjLaOJYD9x
+743nxwYs4BNqmu7R+2WW4KMDTpfGnwuY5uhcqHBFPLNSNh0niZivzO0lV+AHAxKp
+BqAZE3TA9VQTayRFgp/XSd2RAntld9HDQkU3LBsKzmhnBF3xamLuCwtrm5a6wv4u
+bKW8LpgPOxWFMiLmcDOsjuhI/p8DqMzxexkBf3V/6P0ZGpcyI0X8Lok=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIB1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIYz9Ct1ZSF3cCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBKgQQAvCaphXko8zb/53P2CmB+wSCAYAMCgKW+nkE
+BAxYuyXOc0IsTZMNh7gvS6coSNo/ukviJS5XNFH8iteaF9N/4y2zUo2Z1Fk2L9MY
+XPfZ0wJK1C9MQgEZrJu71d6duhqIiJPoiFM0L7czt//agaPRyblyWSOxhAAZNJeB
+xA0kXD1De1hatge80Pfw6C9uGJok8zwl3/N8vn0TdompMFSh3kTVwgBVwRwWzq1+
+erf9Jg9xL45gVp26PJszHZzfyQCZWOC3a/sivaIeNlu41j+wpHy8gyd2R7HeEAyP
+C/ALAs8aoIGHS+ug9JzmeEPQNQ1g6XDvDFwmL4EmVISrTP1h3eauPYUQXmoCbgwD
+jKJDZCGVL08MAyb3QX6ceGJmeEJ8oDZURveys7A+j95pw27wBISWEzuXC4dhG82C
+uYSRm9RiWepdyE5Mj4aBr4z4c5keBsB8ekD9D5tUjbLMWt+ZC5RYGtNyv9bClKFI
+Jtwr1+abNauB26AoBzinCmMbaAyRr/ENKEPhWJuq3OozvIewW5RJbAk=
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-epc-params.pem b/tests/files/ecdsa-epc-params.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-params.pem
@@ -0,0 +1,8 @@
+-----BEGIN EC PARAMETERS-----
+MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////
+/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6
+k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+
+kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK
+fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz
+ucrC/GMlUQIBAQ==
+-----END EC PARAMETERS-----
diff --git a/tests/files/ecdsa-epc-self-signed-cert.pem b/tests/files/ecdsa-epc-self-signed-cert.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-self-signed-cert.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICfjCCAiOgAwIBAgIJAKASPKiQipNtMAoGCCqGSM49BAMCMCExHzAdBgkqhkiG
+9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMx
+MDkzNTE0WjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMIIBSzCC
+AQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAA
+AAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////////////
+///8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wST
+amZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP
+40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA////////
+//+85vqtpxeehPO5ysL8YyVRAgEBA0IABJGPZIpCgSCPPr4phDHNPhrKhmBNMIFh
+ZjcSJomXmiTRyohTtIFo9036pNVzMIyNS+t+WTzAOYqaB2tVq4sf+XSjUDBOMB0G
+A1UdDgQWBBSC8kx1ORBUpK2yIZjT95j80H+cezAfBgNVHSMEGDAWgBSC8kx1ORBU
+pK2yIZjT95j80H+cezAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQDZ
+oc3hPRA/zknb6/PZGamT4MQnPBvwW/0MaPiDBw2JJwIhAPBSJdgVyZuyJlzyhDFV
+wwodd6UxBRuQ+rXatKcA4k1p
+-----END CERTIFICATE-----
diff --git a/tests/files/ecdsa-epc-unencrypted-pkcs8.pem b/tests/files/ecdsa-epc-unencrypted-pkcs8.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-unencrypted-pkcs8.pem
@@ -0,0 +1,10 @@
+-----BEGIN PRIVATE KEY-----
+MIIBeQIBADCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAAB
+AAAAAAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA
+///////////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMV
+AMSdNgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg
+9KE5RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8A
+AAAA//////////+85vqtpxeehPO5ysL8YyVRAgEBBG0wawIBAQQg7AVNdkUlBnl+
+PDsD3TPNSRxJFBDp3BzrGfKxUQee/uChRANCAASRj2SKQoEgjz6+KYQxzT4ayoZg
+TTCBYWY3EiaJl5ok0cqIU7SBaPdN+qTVczCMjUvrflk8wDmKmgdrVauLH/l0
+-----END PRIVATE KEY-----
diff --git a/tests/files/ecdsa-epc-unencrypted-trad.pem b/tests/files/ecdsa-epc-unencrypted-trad.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-epc-unencrypted-trad.pem
@@ -0,0 +1,10 @@
+-----BEGIN EC PRIVATE KEY-----
+MIIBaAIBAQQg7AVNdkUlBnl+PDsD3TPNSRxJFBDp3BzrGfKxUQee/uCggfowgfcC
+AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////
+MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr
+vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE
+axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W
+K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8
+YyVRAgEBoUQDQgAEkY9kikKBII8+vimEMc0+GsqGYE0wgWFmNxImiZeaJNHKiFO0
+gWj3Tfqk1XMwjI1L635ZPMA5ipoHa1Wrix/5dA==
+-----END EC PRIVATE KEY-----
diff --git a/tests/files/ecdsa-p256-encrypted-pbes1.pem b/tests/files/ecdsa-p256-encrypted-pbes1.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-encrypted-pbes1.pem
@@ -0,0 +1,48 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGwMBsGCSqGSIb3DQEFAzAOBAjX1Gfd0N4/lwICCAAEgZDAmo+X7o6E2jHihQfA
+wJhosfs/fOgvWXCSUFHJM6dDgvn8EVd8pElw1NUktV5cBIcL4HZpTmiZaMpDIXNj
+L9EkLEszXMY1wc4qLixGlsOpB5/KCBAX++w3Ur/vmzn5Zxh0vVjpjxNoaKjoTZH6
+YE6fbFvp14vnKg6Uexld7k3efJY0PCMStcust6bLWSWikmU=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGwMBsGCSqGSIb3DQEFCjAOBAip8M8cWxEHeAICCAAEgZBJiHgjgH+b7gnFJreI
+iWOhI2sIDoiApQXji9QeyOsaWWhygR5sVKL4gkyzQcicPuz/OVYWQPGuMr2Qdvxr
+SuVjMmEBBh/sS0mWimxyrbnyZfwY7ZHMd0wWEBDni1KL9vIVSPWLhwfHvEFgdHlV
+UnfpWGUoEa2zjpGSCKOy/kaKnrQYeyKUdPaZjEvPU6aJidM=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGrMBwGCiqGSIb3DQEMAQEwDgQISqJaePRS6sMCAggABIGKnZo85GQkubGmIL0f
+4RzIu9kERwTSgvnjH7gfW9ZvW15F2GruSyIRiuk1FhsHQhb/H8njlD2qkrYI3f1/
+b3g5L5r5g+D/9C6C2ArbO3uZpvQor0pUyuKlESYd4E0LjveTKmCGW+xQRlmjwWZ8
+XRMb+KakbQ7TL1lGLew9P3CIDAj7TxjTLSLwBa++
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGrMBwGCiqGSIb3DQEMAQIwDgQIKUAgsPo+gu4CAggABIGK4j8mtUNzJm88CGuz
+NZbdzJpq+A/jHUKjJaTNhN8pUW2n/XeGmozPhRSQzzRHPMLoXUMUYw02ttJVVOTH
+seTcRzy1EQwTTmVe1MqL2FCffUCYD+bO5h9rO1AYC/hX3vr9+/O9KCj9pgTlG950
+nQjb5f9wsE8BpMMf5z4e76fyoywdjOON7DOrtRoQ
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGxMBwGCiqGSIb3DQEMAQMwDgQIcndWv6ERzNcCAggABIGQDiCUJfL0YnPSV7xA
+Ke/bOMRlidthv0nRzQxzqaK/YkNPUQQ6d1RRTigXT/k4yBz9DH6q4sMpnjf3MyCw
+YFSXcQXZLnuq8zwjo5PoFyHb0U6QTdgd+IsegqupmyvzXOvLPcZH3/iRwNXqABip
+h5E8wPBSzobu8QztTZ/SOBnbyY3ys4QZckM60zLRWabE+IJ9
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGxMBwGCiqGSIb3DQEMAQQwDgQI+DkAP5uNPwMCAggABIGQ/eMzhfa98PWmAzqc
++1ti4VrxmHMwzdIGygpVcN8WhBbWN2mChFXE4ZGJIJYmpJyqS23aTIkRTElL0GuB
+WxcU/3uQ0CwdLtgHooPhGHkYnbHw8LwblnX7ECdt907KrRnqNHJ3BIFakNfvmbdN
+LncPFRdoJ3AMd3maXVvTyE7j5JF8dQGDQ6+iknmvd8YQHslt
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGxMBwGCiqGSIb3DQEMAQUwDgQIPxLQFIF8BHoCAggABIGQWAqlUpSLKexDJqz/
+3CHUsHnN0wtPxfe9DeDOB6Akl3I15HztsMh1jr3yvyV6yHCBe1VY77Wp7CTb3eAB
+f61rsMb3iFvgwN7ba87qnpg4+97oxU3UpEqpiIxu82NZzModHtqXcHwZ3RT0rc/f
+m3skEYWr/kjZaqCG3Qy08gdC8ObNfZbOQgFudXkFDmXb/64V
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIGxMBwGCiqGSIb3DQEMAQYwDgQIjZUSrsEemGsCAggABIGQQ+r2iokCHNV0ry88
+w094AZOro+EJmNzCbXHbMfLj/nlZGNaPS1xMGG6kUb4e8JjkqTGbjHCM0ryd3czS
+6KCZeuQqGm2C6ui6rRtfzMedEFFTfmxuXOBYfi1uK8XAIqKJLr/Wzl22j3mSHotA
+n38ybvd7JQFZt4Vhb4moXsGwKBBT2cpwhMcHmspmtwWPeaXW
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-p256-encrypted-pbkdf2.pem b/tests/files/ecdsa-p256-encrypted-pbkdf2.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-encrypted-pbkdf2.pem
@@ -0,0 +1,50 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHgMEsGCSqGSIb3DQEFDTA+MCkGCSqGSIb3DQEFDDAcBAibocShMlF2eAICCAAw
+DAYIKoZIhvcNAgkFADARBgUrDgMCBwQIzyEw4JtA0eQEgZBeh88mnANFl1ZU2FVt
+roZqlNFhEBPo9SAs/HczbLsE4RXypn9nvVTdsKUNZvRvVIEi095YLb3F0hlqK2wD
+tzuj5oWjZpEKsnTZ2w+HUbyMbh1HDqSxq4pcpTHm3djjGuMzr1tBxUecrPV8qexe
+wobqCs+F4scVogwDqs7g10qtNUeQCYNCT8dnStvkVHaoqXg=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHjME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAjDYuulig+s+gICCAAw
+DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIPCAI9QalQkcEgZB79SD4TnF6u3r2
+r/uMo2OuMnqRNw+PixfZnKa/xAxFmhKymKgCnVCXnulwz8fV6tXzFVZ7Kork/s5R
+KUSmcRZPtT1bfRgnTaASsrKmwC01dANRsLA/irmRZNtonwtYhmpkpngM19cGpGKd
+COZW3HdkNpOYif2ikfLqANZ4Kr8BaxHCvBH7pZUnQ/EMDZ+diCo=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCkGCSqGSIb3DQEFDDAcBAiUTRnu/r/JDAICCAAw
+DAYIKoZIhvcNAgkFADAVBgkqhkiG9n0HQgoECJI2zhldluaSBIGQL16UOcQDE7F8
+z++qmBRFh6SzmOqGpVvHZFTaVYPWtnr3+8bmeG8VODm5AwVW11VTXnD0EH+t+bLu
+KBceosNMZNhlKVBdFZD1qdZO6X84UtRq/pehWBw08iMY0JiJKA7bEF1xMJXK3TV4
+wA3K2ryj8r+1B9jDxI19SoTlKpB1WFUg9QFh1g1wMG9cC6JpcBPq
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHuMFkGCSqGSIb3DQEFDTBMMCkGCSqGSIb3DQEFDDAcBAhvY1bt8cacOAICCAAw
+DAYIKoZIhvcNAgkFADAfBgsqgwiMmks9AQEBAgQQTKETRz1UrWanXQZlKfVdtQSB
+kCL/kpfUMb+r/nEplRgUQZH5y0G5pB5sT1LH3iC2NUAOEHhEBr6Pn8akByBDCrRY
+h+rO1SJME5oo7polnb6srxnBrkjrvKYZ6ne/3yG0LkYAKgF6nRmqNIVJVfees4ZT
+LjeuYMNYmgMF4HpBcpkMz7zafYeb6NXQ1UMasHN1mbIUwTPnDN8RlfssuWHLGqAs
+cg==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHrMFYGCSqGSIb3DQEFDTBJMCwGCSqGSIb3DQEFDDAfBAjYLsHJCH856gICCAAC
+ARAwDAYIKoZIhvcNAgkFADAZBggqhkiG9w0DAjANAgE6BAgsrVp7+I4/BwSBkBbz
+b+11SUaYAUR28juF71s5ikLSUa7draImlFNYkAIlr5HsTvyeyzKFhGW7u8zlJZkm
+eYofzJ+2J4IPN26rgQ3/QEhVvlyvCyByi1HDdMuANsk7tcRrdEXotXAwQNuOteE/
+/DjDWwoiVuqXVGbsgk2ail4TwMyGLCVZhCbrx/ovsLekDB1BJFWjXIPHZFVfbA==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHsMFcGCSqGSIb3DQEFDTBKMCwGCSqGSIb3DQEFDDAfBAhfdqSzavBOTAICCAAC
+AQUwDAYIKoZIhvcNAgkFADAaBggqhkiG9w0DAjAOAgIAoAQIERLCMbZx2s8EgZCc
+ua7hkVEe7aHr1HqxKgFirPbdRB+P0ov7ymfUkfiVeUuKbLUE6Wpbf1EAIskbAFYT
+dMaBnqbzfbAZcKNL+1lM9xvh7yg8jWsFn4FqAezrYh8TTb+OHNBKeXGuk/qsPQ2W
+haxw8HAIx0Ykh0XBpAMs2hPh33FPqZsbXiRVG3zhHuD0ljCkpymx+ViIQGB1wNg=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHrMFYGCSqGSIb3DQEFDTBJMCwGCSqGSIb3DQEFDDAfBAirhA8ZkYpz3gICCAAC
+AQgwDAYIKoZIhvcNAgkFADAZBggqhkiG9w0DAjANAgF4BAhfyCO8OozVEwSBkA7l
+lvkcjvvGwVZuhKt4B8megLXjnBTGu9Q3mhVAjIx05SfjEht7CGpNmFEZwgsITZ6l
+KTdFpjvw04q8J9auin4Cx9MO+wO1bXujMfHe+uVITyfFuP+s0riRmXVpLu/w8Gsz
+v41UEIOGPzyikE1QR7YjZBXbmKiV0I9g+ibMzI+/8gL/OzIZqdZDLQNfgIKcPQ==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-p256-encrypted-scrypt.pem b/tests/files/ecdsa-p256-encrypted-scrypt.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-encrypted-scrypt.pem
@@ -0,0 +1,21 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAjA6KltctaCVQICQAAC
+AQgCAQEwHQYJYIZIAWUDBAECBBCxRl8OYcFksxBJJa10CqfFBIGQtgZJGAiILkm+
+WAwjhv4cFYw21cC2CfWqf+5Ns1hGIe44C3wh6isgNbk2EWD41JFNs3Kh8ov9yIwj
+mN6qfNWI+zhCiJ8rqJp7jmec/xg+QcPX/57EFSoJr09YjhAj/rABTornyN0BU6qg
+d9Bbqe0fKVK/YnChjvMKjdcpKLOJKGEjs50AYFdpWj3DN5x4+osj
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAjFtKsLQwg5hgICQAAC
+AQgCAQEwHQYJYIZIAWUDBAEWBBC+UkFqffq2uECRTJE0eWzoBIGQC1wqDWORz7KR
+5nYrSt5sotzD+NQ3x0kR4+m7lAJuw71KXGFI2uYet7FrxKmR6UFFNcUv9/HY/TnV
+iNa9SWXNEPTPe+7Uyy7dHrNBdP2EZ4b1+D+EA67pH2KGPZUOgEkvNR/12Q5X4DdC
+XX6ZeruqOXIWu/t1A9KOXfbYwy15a+KoiseRnaRHflunbBx4Snkg
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCEGCSsGAQQB2kcECzAUBAgm/Gwp4kcdSgICQAAC
+AQgCAQEwHQYJYIZIAWUDBAEqBBCM3Ciqoy4d+2wE7C1lOceEBIGQ2RENqgg/ZXwD
+UtHVjHM7zwY1ZC9EDRle+pAnL/nPlGk0mnzbHt3rWZVFXVkZsTOFZMNfNYl0/pDy
+xwKJPp2gzOuxegNsk9v4CV/s8vxAfZEEa9wv8nfcp4F/eaaSU4cC4ofXgDp6+WAz
+uaMPf2yzdEdgaolfJudbYlqJ9Ufra58HctZwJR48JLTYSYBuBpHz
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/ecdsa-p256-pkcs12.pem b/tests/files/ecdsa-p256-pkcs12.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-pkcs12.pem
@@ -0,0 +1,305 @@
+-----BEGIN PKCS12-----
+MIID6AIBAzCCA+EGCSqGSIb3DQEHAaCCA9IEggPOMIIDyjCCAncGCSqGSIb3DQEH
+BqCCAmgwggJkAgEAMIICXQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIFH0q
+sY0hx1QCAggAgIICMPbvE6ijIbACCeQG+LUnydn7HbMHXK0C9fl3+gT6I3j01OiG
+b9RAobZtnwLV1Zbvgw/hUMG3YMKJZ00ydG+C54Cvn/Ta7O4NJfrxJXq2IPL/0RZW
+GtnbIMm71NL5c4wmbbn3nPXWXbcpTS2TC7bnLc6N+ePjKybtynqdFKcRQxiiLqtl
+yzlh7nym+kJV5KSdaCOSUArU699Ymk99OAkYrJhceIJo6v3A0oxxTfYnoJjuxVIM
+sVo3GTfK+kFORJ3Spxia5vCPoN301hfOaieCrd1RpCaB6Q1Y50nJbZI/bjQ0kjEY
+O+7Cnw5XYl2AHHCTzcQ4mwzDDD75r7E4R7tA/XuTbmC6mFB3TyBFtsUtXZtCRFWD
+vrao0ae9sY7GRe//GTI587ZQAXRaLgmbEnUGMkMubsrh/xwPSEDtpzvPtZW81Qf2
+krkO0rwb7FJTzajoklpsgFgVw/VMKNElAO8YSNJ+JX/sagldvfjNHUIQMSgUignW
+dQzSPBRwdOfdjqwgaYVwcNJ0JpgdMZpomNgJmEB10GJblaKXyzbRSH7AuO8Mrr7+
+lrriXEinopnJCAb6YO+Wzc1bsSeVLu2ofeHblbMWb3yofLCuTtvJXR06Xs+ulIjx
+J/sGL/P/JPt8LsnPIrGd67dHy3ChmTtBPNGjDoOzZ+EOOQmLTnsVHR6LDABTaXvR
++U+MHjc9QIpWVxaE/osyLhBPOHIDqyIGAX1WqHa4aPUzNxwgXn6TMgIfoUZGMIIB
+SwYJKoZIhvcNAQcBoIIBPASCATgwggE0MIIBMAYLKoZIhvcNAQwKAQKggbQwgbEw
+HAYKKoZIhvcNAQwBAzAOBAj7Rnc8QaBBAgICCAAEgZA1tAy4JyoPS9EXxO2e7Vow
+MtDsVwyJzuitmD44ICjPxrJgmyKBFtGDLXBfadLI/VuD9oqlI8lP176hTYwwM+72
+/hwKGXmJfP1s2r7sH28sVmbJO2dYjQXtaanyQ5aDGaFMbcMiuPK4y0vB98TyguD9
+S+mWoT/MVE9F20xh82gxQycHhpdT664UxeA6YBJru4sxajAjBgkqhkiG9w0BCRUx
+FgQUSzL+96khVinAc/v3rkVpgmeGLu4wQwYJKoZIhvcNAQkUMTYeNABQAEsAQwBT
+ADEAMgAgACgAZQBjAGQAcwBhAC0AcAAyADUANgApACAALQBuAG8AbQBhAGM=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIELwIBAzCCA/UGCSqGSIb3DQEHAaCCA+YEggPiMIID3jCCAn8GCSqGSIb3DQEH
+BqCCAnAwggJsAgEAMIICZQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIzybx
+opbqZnkCAggAgIICOJEdutTDfLmiKQEJCzoT33Z6rEqdA2STzm1qL2Ir9RMTl2wL
+zKqqMT9ORFErYaYuIjatj4uSxn//JumW7/fD1d1fJeY+0KajhGpMbp7hmU/wn4mL
+nyUCcSqI+2VoXjQlmH2yRFTgfSBU91+4caap4HN/wGefTZsRG6EwyXkHS7JV9Ocu
+qm1SYBRjxIcMt/4JSMNYVwuSDffFEIE8U9FjnuJKTl49GFdcKwBRJVUyTnYOlPse
+YtFw7Wt/FESxG1IxGV9bU+Xf/SR1AfVlRdqofoCNVavwd8GFtWEfUdXlHjqo2ufN
+fTLTzxQSGNakGknJHDxQFgUUAkrB+KyWBf+YItGfb5DPp4R3TJJvBH6lNTq1w8j4
+aSncKElU/Wo75ULUmZqKra2B7gnIZ9JdTxijRaHothgwd6zDRVlNPSct+xDg4mwO
+zFtjg4AEyCcK/NjbUsq17qbKXDyLkfwA6P3xQc3gepgj6xG+102WOMu0ZD25ucNa
+Te0By0PjkrSrP2kW7PFnBWZFI7abNP00JRvQ0gLuj3ax8JW7Qie6DEOXOPqeeljK
+gPguFb9GAX3o6lZed4K9dIMXB4btyl8mA6PIIPr23hHnyRcqno/Eq1FQQUwmN+XE
+o7s22Tc/y/DQ4fgiMZkFZhW7DweRPrx4flqRmp8QIaG05wGgMW9q+u7s5Djew9kA
+UkHmTAoHbSWal2XXyqkDfbKQp1QdC5fTxOmdFZdQ+xdh/jCknwvKVaDcp0Xs5ZWz
+ZICgq4AwggFXBgkqhkiG9w0BBwGgggFIBIIBRDCCAUAwggE8BgsqhkiG9w0BDAoB
+AqCBtDCBsTAcBgoqhkiG9w0BDAEDMA4ECNIHMplVd+ksAgIIAASBkBjKaNoqEBSQ
+vnDxd3cf49x2w5su0kKScrVMXRjKS0EdhvkEZ/Ybmg9ro4VHvkmTmWv54eY+no3r
+UbvVJ2Btct5LQdg6WgvB1/v1wJGuOH/Y8f9toZ+FjsLsOSkpzOP21iVX8pLJOTvK
+F4dGPyyLGx8xtZZOeA8eevzFlrf99uwlzCkv4lG4ZS3FSKgCdb5DUjF2MCMGCSqG
+SIb3DQEJFTEWBBRLMv73qSFWKcBz+/euRWmCZ4Yu7jBPBgkqhkiG9w0BCRQxQh5A
+AFAASwBDAFMAMQAyACAAKABlAGMAZABzAGEALQBwADIANQA2ACkAIAAtAG0AYQBj
+AGEAbABnACAAcwBoAGEAMTAxMCEwCQYFKw4DAhoFAAQUtk2zS8R+SstIRhAjP6LT
+PfQ0BEUECOhUkmBy8gJ0AgIIAA==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIESwIBAzCCBAEGCSqGSIb3DQEHAaCCA/IEggPuMIID6jCCAocGCSqGSIb3DQEH
+BqCCAngwggJ0AgEAMIICbQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIC5r4
+uzdR0uECAggAgIICQHbHiFhHuPenOSHe/jkUiyUJE2ItJlxxYGWJylZI41Sz5Hlg
+O75NCrJq4WFytBD7qeFt4bdJ5u3L4TmusDhZfjgi1iW2dtPmp+rwksikWITK4eHU
+d8HJOx+PqtvtN+UI9rTObfVxg8SPQ76vR3NLaQCaLtqNwGmAWSzOlivDdXCoumez
+VG7m7CX3luLSqhG2aag8EhV15dBc2eIj5Vd6zSRlybV06/MEdasAoeiARAFK4c+e
+mC7gK1sxvECWnFwCVrYyWEn1EDLvGYjpUnLfHmTu+fV2LFCTxWmV7gXM0tODaYDM
+hsHtMw9zlSAhVa2iBkL37mjPeSM9q6Un6NWw645+8DyTbWxGkrrpm1vGc/xJYp0G
+5t05jzrFY/AQ9USVELw0PzBFwQiGsNxAHkR8z6k9wxfIES9IpdFAbNBzWmurlEtK
+5OXP17hEmQlSc4EaOivN17g0oJxRcTXglHN6zwiP8iE5OfJehHgGWynytJ9wnN+W
+fkQrX3NNVBxXT7jPMgVHkWBWytZeex6Wy4wUb2uKSYd3I3XEMGAFzg/3bcfHoTBN
+FRwPCFX4DyrSkLsFDv4uAwtNkZjRzRbV9mW9hW3chkcNvH7HfS8plsm19LhayqHC
+LnaM45zOmU1R7zPL269HUq4IZ/qEGspwDoN6wTRhbiXZMaYQ7BA1XoNVsTBCzijT
+FxIO7Oi+IJNffXtSW6AuRtq7I99p5iSVrB1QTZr1pVD9KAsCzbGGMnUrei+LFScz
+zuqYmWbsms/WtCqX9TCCAVsGCSqGSIb3DQEHAaCCAUwEggFIMIIBRDCCAUAGCyqG
+SIb3DQEMCgECoIG0MIGxMBwGCiqGSIb3DQEMAQMwDgQI+WOkpHQZfYgCAggABIGQ
+K6ntubGW+rZ1/c7EqUNvRVnoyxJRp/VG+vEQPf/VoyZpAvYGZPzmVseP2nSTZ9zk
+ooElFmVR7tMVi86/wPhwyuotGdpou2s49lx4prH1mjdQY6W+Ybgj5HdI/vdo6Bsr
+rjOewC1j4WG6Gz6lTQr+ADA/21vn/f2ooxHGf3Uh53yPe+MfCV8RYg6c5fhG5CaI
+MXowIwYJKoZIhvcNAQkVMRYEFEsy/vepIVYpwHP7965FaYJnhi7uMFMGCSqGSIb3
+DQEJFDFGHkQAUABLAEMAUwAxADIAIAAoAGUAYwBkAHMAYQAtAHAAMgA1ADYAKQAg
+AC0AbQBhAGMAYQBsAGcAIABzAGgAYQAyADUANjBBMDEwDQYJYIZIAWUDBAIBBQAE
+IOKPMwzYN46vB1aOSoIKQSfDo1A3CJH0PZleWonCGwdwBAjCHBkcsCYB1wICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEWwIBAzCCBAEGCSqGSIb3DQEHAaCCA/IEggPuMIID6jCCAocGCSqGSIb3DQEH
+BqCCAngwggJ0AgEAMIICbQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIn89b
+Z3GPMpUCAggAgIICQGKf889YFGdAEDzO/krxYBRVQaf6j0ZOWH5G/VyKDbyXl8hm
+9A4LQtfQa+q+LO8DhIea9AfA6aK7WKuAz5vnQIA4DpFF9r1qmGOFQsIbzB9Jo/ik
+wRIp8bylPIdwfbyaUqH+0ITun1NjgdZCpGkV1Onay0uUPZq6St22T6Kv5g/b/x9f
+MwaUwikXQVNNfOR+8wrW0djK0MOp9b8zZnmTvUDURK6nJEaDMeQgP0CFcb4F9Xv2
+8r5v56zSU6edePQuxS5uALWSZQg+7TqQlD4d5nwT0+wkIU4QnDMHcZJGwblyunsy
+d5RTJADCrky0V4BTz8l7/avwK66CkNdw2k2dbN3uJhaIjSZuoWkHmD/iAYS7fAQe
+fHwBElEqOPS+KLsBtdOtwyhWnbqIFd1cL3p+ndZn5UEA07PWSHfpg+SejMK8RRff
+HSBGVgdgvbXKH6PKrz/NoJVb7BO8xKJjIdVjC4GOVNlazhFdUNXryMgEC9n/tg+U
+EZJNcMq74jR9tinCnE6sU1IAP8CpOtrMTpCXnnMIBl3JQCgfem7ahLDfY+3YIp8B
+1Q0DnDG441e1iTcOBaxcOPWaTN8Lyc3SDDgE5D9YWr+Ez3s5OALtb0jy1AS6ODRJ
+v1wgunlHqpapH9iA+ArOa4VTXp60MrUqhCdGkMv/EmiDRJ7P4Se/f2Tl7epCPCl3
+/SEy/4EMX0hhpPGAAASaWhhDtTUsuNrX+G71NeJiyDFh/k55Wu3djblBkMszVsgY
+P26MwEnl451bVHGzQzCCAVsGCSqGSIb3DQEHAaCCAUwEggFIMIIBRDCCAUAGCyqG
+SIb3DQEMCgECoIG0MIGxMBwGCiqGSIb3DQEMAQMwDgQIlX6PffUsFMMCAggABIGQ
+JI4+RRpUEffRaz3qRdXoM1w6rHCxyefKTxfRp/jsDvsHkBEOeWFE5cKm+3BXEm0T
+RMGJlxB52ceYvVA9/Bo0StUKuxYw5dsbftBhjSsH/5i6i8DGVOCFycD0C3B1hn3e
+8Xt+kQSCrZcc6vVd8gjiPvHhynPPnn2lli0NlE5j0DH+as3sxjtfEdPeQunKSFE1
+MXowIwYJKoZIhvcNAQkVMRYEFEsy/vepIVYpwHP7965FaYJnhi7uMFMGCSqGSIb3
+DQEJFDFGHkQAUABLAEMAUwAxADIAIAAoAGUAYwBkAHMAYQAtAHAAMgA1ADYAKQAg
+AC0AbQBhAGMAYQBsAGcAIABzAGgAYQAzADgANDBRMEEwDQYJYIZIAWUDBAICBQAE
+MC8cVO84hC0SnVOrqDYNQMWor1IyVaU/CaepQiEq949CrUONcjVM4BfgxoVse2WB
+uAQIkqDilpIjT5oCAggA
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIID/QIBAzCCA8MGCSqGSIb3DQEHAaCCA7QEggOwMIIDrDCCAksGCSqGSIb3DQEH
+AaCCAjwEggI4MIICNDCCAjAGCyqGSIb3DQEMCgEDoIIBpTCCAaEGCiqGSIb3DQEJ
+FgGgggGRBIIBjTCCAYkwggEvoAMCAQICCQCX+mbx0uA+ADAKBggqhkjOPQQDAjAh
+MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTE4MDcwMTA5MzUx
+NFoXDTE4MDczMTA5MzUxNFowITEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxl
+LmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHYqvg1E3p4xXBUK+72zfa3M
+LmJIkfZDsYqRsZwRXJ7TzRxsbeVgBDVy3Zj6Y4ZSB/dK7jxs54KTECoDcXLvEASj
+UDBOMB0GA1UdDgQWBBRT2nMGMWO1FT6OJajOnO9gp1hHVzAfBgNVHSMEGDAWgBRT
+2nMGMWO1FT6OJajOnO9gp1hHVzAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0gA
+MEUCIAvA93qcJRkeyRffr9IKzYQXkZE30qnCBzlEfmfUClagAiEA0GQKzKiwGTv6
+uV1pwjprqyI7h0dObEESk3KMQRa+5zcxeDAjBgkqhkiG9w0BCRUxFgQUSzL+96kh
+VinAc/v3rkVpgmeGLu4wUQYJKoZIhvcNAQkUMUQeQgBQAEsAQwBTADEAMgAgACgA
+ZQBjAGQAcwBhAC0AcAAyADUANgApACAALQBjAGUAcgB0AHAAYgBlACAATgBPAE4A
+RTCCAVkGCSqGSIb3DQEHAaCCAUoEggFGMIIBQjCCAT4GCyqGSIb3DQEMCgECoIG0
+MIGxMBwGCiqGSIb3DQEMAQMwDgQIQrFaHl+yq4oCAggABIGQQw+qnMDWAzFgExmo
+n/TFr+viTkGMvUHp1ku0i/84uJ5qZh8M8VxNz+kHMdqpICLo1QQNjzUfxNZGcdrG
+9d4cUPTBJolOnAUgIJsB92V7gbrsfou+sTWe3Tcg71hVQIgGgrJAKuKFIyTByviX
+m4Dm0TzfxmNJwK3RU8YuLnJkm3QHmGUYPJ3IJqFWNGYVXiNbMXgwIwYJKoZIhvcN
+AQkVMRYEFEsy/vepIVYpwHP7965FaYJnhi7uMFEGCSqGSIb3DQEJFDFEHkIAUABL
+AEMAUwAxADIAIAAoAGUAYwBkAHMAYQAtAHAAMgA1ADYAKQAgAC0AYwBlAHIAdABw
+AGIAZQAgAE4ATwBOAEUwMTAhMAkGBSsOAwIaBQAEFAKLUf6d7uvD54jrsI9j+DpK
+2CroBAhjf4XZU/977QICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEkwIBAzCCBFkGCSqGSIb3DQEHAaCCBEoEggRGMIIEQjCCAtIGCSqGSIb3DQEH
+BqCCAsMwggK/AgEAMIICuAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqG
+SIb3DQEFDDAcBAiggrBG/lAmGgICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQME
+AQIEEHMNnXuYwnMbjOSFmm5xEl2AggJQRvqqPaLjdtyoaWULLjqu1g0bPuZuwsV7
+yBRI3/2sTrFfqBasHMl2nQOA3udFVtlCq9lUuaI/vyDRTqlM2xKqjzvoeFVCoXEB
+qGBfYMPova4443zYYczv+WJLi56Y0z/rOwon9tZfQWWCPrRhhp1w/CqXLeuK06R1
+/+AvNUGK9ihu3/DZ9wHTGHcfs7ciLe4U9YubiVHXwLzhMxZMvXgfs8ogXcDlckrv
+xKuOl7Fww92TKni8IpYU1PWpeD5Qb5IYveaEGF7ELw4USHfYG38G7pBnj1Yy4+KT
++SC9rx5ZCbuELGtoWTJlcvI54kkLZ6sn+ZqxVVxOZql71f1nUqLiJmt5V8r6kgEt
++ARauqbrNMcPm4kPJmTADCLYLtYyriQyqbvb/X0bXgAevND/mrl3XTrqj1SwXRxP
++380fXmNYlz28QFgM1Z16RjxLfGzo/mMQx/vT27oGbwOkar8+wqcR9acZnjuVDEr
+Fq3b0ski6mn6gM73H2F4xzv30UD+BUxgg/YJbcgLiCNBNMy5cr06j4ebvDKGWbgj
+qAZBShbgOzpYMhUpX+KfN0eq7k6CHn9ukn3s6A/blak6oy7Lf+pQfg/ZxubAgA1I
+q6Z19FH3QS6WYOaMvXLXKNe6T4YLtnu154iV430GMCEwhhFVL82ieuCKKGWXJey1
+yQZAxr4m05rouZ6ul8rd1bxTsh6AZlZ5ylIi6pk+h7HD1cZxYthvPnrkWrAI849e
+WYUu5VBykQUMTt6fUUltQiDTez9FtC1RN2+9qFrJacnxFm1PEPD0QjCCAWgGCSqG
+SIb3DQEHAaCCAVkEggFVMIIBUTCCAU0GCyqGSIb3DQEMCgECoIG0MIGxMBwGCiqG
+SIb3DQEMAQMwDgQI5D7IqrU5mUICAggABIGQh+/XDU/wsMCf7Gh0SY+9okbbdgLO
+Hj3PlneCOMeJMJoIK2upJUU0MHety7Xeu8Xv4jMcspCTTqbIBwJtkMdZo7M7JlZ2
+LZ1DgG6xRXYLTYUvWK5aTXZqwISEriTyWNetpBrfuCK48tvo/FP4WmmDHWwVUcTB
+5g/Rsvdcu/c8chKWD+m4jtaXG1fO7kW9qcK9MYGGMCMGCSqGSIb3DQEJFTEWBBRL
+Mv73qSFWKcBz+/euRWmCZ4Yu7jBfBgkqhkiG9w0BCRQxUh5QAFAASwBDAFMAMQAy
+ACAAKABlAGMAZABzAGEALQBwADIANQA2ACkAIAAtAGMAZQByAHQAcABiAGUAIABh
+AGUAcwAtADEAMgA4AC0AYwBiAGMwMTAhMAkGBSsOAwIaBQAEFJOaeethrlOXj6Bd
+WTDuuxNrbxOTBAjuBPHa1rrDLwICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEagIBAzCCBDAGCSqGSIb3DQEHAaCCBCEEggQdMIIEGTCCAp8GCSqGSIb3DQEH
+BqCCApAwggKMAgEAMIIChQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQUwDgQIout0
+wIR7cikCAggAgIICWC7qqjpGtFkZwlykv0FzU3eNU0c0L8Et3yaH1frNkhpnX7U0
+oksJ55AsKNCYHxjCGfBi4/pFLJjzmIm7jw1EUNpw7Tz6/8xwov9JS7VhHLgIJ+Ak
+SGUOEcrGBEtCWcwpovXlkqoawvmM304m81b2wSjNeOD+gD6q1nx0s6SOoy6ZyUK1
+bSGnK/mdR4WEs2KeQ/MZK01UR5XI+E4qX9tmF9yL3IU0yokqWY91XKBR+vS4+pRd
+YxVjKZYoMtuO+QdczAQSa4F36Lf/IXW7N45HeCnijCVWz/9r7S2KBjlDQ1duWF5s
+nHucl6VTX5vr41hcTqWH+DQCi2w0y2zbAyqwz8xyronh2OF0mus25rKBuXaxo6BQ
+C4msotkVk7K1E8d9Mo9HbrbjTr6q7q2XRTsD+YKFa8TcDFUTZ5gYsmpUAjevR3Hc
+S5Db0nlLyMXJAasBFvuMfNKoCqWkgm2abrYAd6ROcIK9Mf7Vvs/Q++ED6PDDsew4
+7TIleg7lVJ/VvK1Bf/r7w6ISR3R8G5rpjYX1Z5UKBx9JGdRsE92qfI3cpXy7jTo7
+EDlvIXK1EE0iVOAbDRphQw7z682nzdqAuwwSUvLYIlekxjx+Trtig7xLOrnl0ARI
+ivqCwqCWf+BeD6fC6jqyC75s97PRO1CwyrwjermHqZ29LAbzlsp3su4UgE/q+8zT
+C1HeK6uSnOvsAXCMGyLI9N6w+IK0vMhYY2v5KRqJE9UTdJ/99t71rWooBkZfemF6
+Kk7A8Q0a3x7bTyN1QgOlO9cIIeHkSYn65ObsWankSYm4Nff+LzCCAXIGCSqGSIb3
+DQEHAaCCAWMEggFfMIIBWzCCAVcGCyqGSIb3DQEMCgECoIG0MIGxMBwGCiqGSIb3
+DQEMAQMwDgQIQ0XzzqZ/BJkCAggABIGQH/cfMsPknp6dnjzVV9kj1rxmqLvkxB30
+jCqwxsaOVJmRdPc+lRU3KTOeltejSHgPP8K/v2F8VqQZA0yXniLoNsL4Rlx1GLp7
+qXbxZ/oD+3+ESLJyTjB74uVpi9L56ojJpvjYtZfHzlMBZtEpZBYO6CJh9dRVJt1i
+ODacdYaAmvTZ+Gh6mT87AKugwDuLiQhWMYGQMCMGCSqGSIb3DQEJFTEWBBRLMv73
+qSFWKcBz+/euRWmCZ4Yu7jBpBgkqhkiG9w0BCRQxXB5aAFAASwBDAFMAMQAyACAA
+KABlAGMAZABzAGEALQBwADIANQA2ACkAIAAtAGMAZQByAHQAcABiAGUAIABQAEIA
+RQAtAFMASABBADEALQBSAEMAMgAtADEAMgA4MDEwITAJBgUrDgMCGgUABBRXtjhP
+ScsUZNfJPD+XGatgpre9TgQI6/JvBnA5OYECAggA
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEYAIBAzCCBCYGCSqGSIb3DQEHAaCCBBcEggQTMIIEDzCCApcGCSqGSIb3DQEH
+BqCCAogwggKEAgEAMIICfQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI+UgP
+cFrlQ6MCAggAgIICUJKytD8kl8Tjhl80ilyB/NxrxFUoFLx3ySy5PVMNEzpOM2Oc
+bHDvAVle2ehL1ZR8c812a4EkNeKAMaRi9ymiRRrN+FRqmjfVqt+lPQidwXkpnS/b
+Nd3nZvKQVsp3sQzeNNkBMobt1eUxA6mzp92/MZSQPvAS0tU92aNDj/PqiQzDA17e
+qEn2Ns/u8JSzvk2d2fRWYeAdgibFebFxyhI6rdJdenE9RNf7JgALJiGZghM5f7NM
+734cz3v1eP+CrxwFq0igXQW/qDqky2xe0p1k0uFwVjfs0SfajtBBSKXEBA88bTnf
++NCNiX7pzpp/eu8re3oRvx2o17FDxdezLj1Ez4zp1yM+O35abSpAIRW+pmSukqLy
+1t6zfETxlSOwKGEGuX81rZzag4OT0zlmayuXijQfdn2Uv1dYoMvJRbyfIOG5Wbg4
+6rWXIIy5AG6+nzzYWjMBh2e7Rts1rFrhx0hTklBpLJLeQfTnlJrwQN0SIdUIN9gg
+4oOFRqrS0BUdYYi8qhizOoLeju1AUZFDQ0ShBC0kXAAYwsKhxvBANYQDi1+h0MY+
+EAXJLGoSKgXqRB3F0jsXFlcGg3bPTfY8gAs9QBGYv3jfcy4A6fxNbXvUFOTRfoUg
+ZGDXZLysvqarb45E36Mr3BtbvOCKLG5hmazAPteUpdcKepgAM8WBoWWIfoPcOh29
+XRlIx+x93kjrQjDoo2EH8SmfWrh5vYIsDD2K9fozg844hZqq763SDXVYQ+BmTJWu
+4XHaEu6wCyGX6AeE7flDl6Bc/1th2yfw8KQsF2MwggFwBgkqhkiG9w0BBwGgggFh
+BIIBXTCCAVkwggFVBgsqhkiG9w0BDAoBAqCBtDCBsTAcBgoqhkiG9w0BDAEDMA4E
+CNNFERdaKdyRAgIIAASBkAcC2AyLZzIegZfk/G8eA0EMOyjaneHbbqDficRNg0Ok
+PkQXZEWNddUdgBggNwo9Y4rGUooHpxnbDEsmgh/iEvnr0UoqZ72uLGOidNnjqfy8
+hOE0jotsl7LI/hIlH24Jniz4jwOoYhE94iEM1JfD4PvRNrbnBfLUcJ5zGCCptdXq
+EVloYf236K3tHSXRADes+DGBjjAjBgkqhkiG9w0BCRUxFgQUSzL+96khVinAc/v3
+rkVpgmeGLu4wZwYJKoZIhvcNAQkUMVoeWABQAEsAQwBTADEAMgAgACgAZQBjAGQA
+cwBhAC0AcAAyADUANgApACAALQBjAGUAcgB0AHAAYgBlACAAUABCAEUALQBTAEgA
+QQAxAC0AUgBDADIALQA0ADAwMTAhMAkGBSsOAwIaBQAEFB+JS38oqVLgJ/LV3sE+
+rTlusdnvBAhD3Yt2Q836RAICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEBQIBAzCCA8sGCSqGSIb3DQEHAaCCA7wEggO4MIIDtDCCAn8GCSqGSIb3DQEH
+BqCCAnAwggJsAgEAMIICZQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQI36cy
+Nl2ETYkCAggAgIICOJDnrzg0U67gXWn/F3HiUgpK5ZhdaTzoSYoZPEVgCXlx04fa
+WADXrFywcbF2HTU2wiasHws+9DHqRRsCKSzd7DcWiniSREXenhhl6ZjVUL1/kDX9
+uVh8vpetzyIv5eO3LDvJw3TSbBBptH4vO1FCJSXNYA7vQCnKVBOxSpli4TW7PUjZ
+TcQMofIHeFT11HCtlZ4cdV6NqePvc9m0UkyU8bxi/2ynqIyf4YtQiIBa5NsxXWOt
+ZaKw2o7dKK7Hr8STnuLaSdJOkY7qh2TBeFfXNe9rjG7bUM0cjO+ELdHoNM3aJ1l2
+T5Zx6kLIn99CFLUB89yt6Z4MideotQQA8K04uwh23vd3FDwj9G31u2Cm/sEgeO/g
+5dataSR/Eo4+cDTLE8qlNjTBJdjlz2qVVPr/x0eMbMAc/WnDmvtSZEE7dzQfawtw
+TubaChURw6zdKKwNOFesNvfDyIN98lsLuOhAWgNwj+gcsvA/MFBnodPTLiUEEMu5
+R5oIjXmI2JYTcaPMrQX3dCgGCrOW3ddq2at1d+Lbnhvv2jPMCpesBjMPwv555GNj
+O7tculdYrKYh/0WiqRE8AF9M/vlPomNIk0o6SXTGew/7VRFSi4O5zeOmSrxbiu2x
+MORhd/fycw0eB/OIKQUYOxBGsFLMc3HNwGSFtVqRJTO6v9F0e+QtHlcD2VWTKqR4
+w+wexcarA6N0qlDOCBGRAS3vJ5d7YQJTfV/n/4SnLDmWIhsrvmwwNiie+Eea6ABm
+JpzknGQwggEtBgkqhkiG9w0BBwGgggEeBIIBGjCCARYwggESBgsqhkiG9w0BDAoB
+AaCBijCBhwIBADATBgcqhkjOPQIBBggqhkjOPQMBBwRtMGsCAQEEIN1BcbroS4Rr
+SL98OeRm0Hzo0POI2k/SkGxKn2EObv3/oUQDQgAEdiq+DUTenjFcFQr7vbN9rcwu
+YkiR9kOxipGxnBFcntPNHGxt5WAENXLdmPpjhlIH90ruPGzngpMQKgNxcu8QBDF2
+MCMGCSqGSIb3DQEJFTEWBBRLMv73qSFWKcBz+/euRWmCZ4Yu7jBPBgkqhkiG9w0B
+CRQxQh5AAFAASwBDAFMAMQAyACAAKABlAGMAZABzAGEALQBwADIANQA2ACkAIAAt
+AGsAZQB5AHAAYgBlACAATgBPAE4ARTAxMCEwCQYFKw4DAhoFAAQUjqGpn9xugean
+d4IruG5yTl2yAl4ECEFTBPWXAettAgIIAA==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEiQIBAzCCBE8GCSqGSIb3DQEHAaCCBEAEggQ8MIIEODCCAo8GCSqGSIb3DQEH
+BqCCAoAwggJ8AgEAMIICdQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQImZIF
+k9BCNzQCAggAgIICSKgLbeRZWeciVOjIMDaAOkLV0MP2cVad59E767+6nlAsNflO
+qidcopd5snW9RUQ+eprSx9DAS2Rhqax1H3QWskpGDIVdD91LOHUhDUqx2akwDD2C
+sOPlooaaTIoVZ9zbTpl3JDdJ7NHdRyHoWtbCu1xEvK+akcqK2fLvI8xwDSwU/DRE
+RU7ELR7GX/jW+WuOvBdBGC8RyetVlC93sUjW7Cfl1QOTbJ28A/UCgD9zANXmMRez
+nEiRw4iG30UX71EBVUJdNvLREqSF2l463G/dTphr4ak5IoLACHuQlVYfOWYCevXN
+4R9LDY1pB56XedHkPATcrXAwxy/gapdIdYdVbUAIWUQzxXxtmZgeoBboVG68Ceq2
+5zsM/Ktl3jnnAwFPx2EuuLHV+5yaNOGZFkz4NsFc0lSMlI24tf6J6GIXo4Crupw/
+dywPn4dX68ILjdSiF9xf8lB4xysVEHfG8CWJvCg5KLNPgvRszCxbCDoSnZX4VnSy
+Ra7Dg7x4ZWQyYCgt6wLoqEuiFTxNhtOI5UMxZDK08UhKdKOPWAEZnuZgnUC2Nnpd
+6Bj/Gtd4N+SjqATYCACQDTIP9uDj7iMsTBmIwK3WjPF3hflSsh6ekIF1J/t8uG3/
+lIDfhZnwlkj3Tli8pub5A/kIyPHyhYxUZaY+7QTwQB1Fws4v9K+nj6QCduBuV6or
+MObYTnrlzz/sqKxohlfaiZroiJmYicjgSHiziBo/fE/miUgSWANin5quolzB/HY5
+eZ0k04F9Im8xLFCA/PIEzt+SV3XNMIIBoQYJKoZIhvcNAQcBoIIBkgSCAY4wggGK
+MIIBhgYLKoZIhvcNAQwKAQKgge8wgewwVwYJKoZIhvcNAQUNMEowKQYJKoZIhvcN
+AQUMMBwECIpOphpdh/ohAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBAgQQ
+Juna4/Yf0krRZ6mE2g9HKQSBkF8wVESzEUBn1Dw4eEU+2Hu5/Zl2pk082BCqAd+W
+ig4eOZ1w2E0/TWSxYf3mzoLmwCrqn0GSmPfpK8D8ulP1MU6fr89YI1QPL5PhV4WU
+NHI2Og5dRagkT8wcptOM4CVHivgbrceTMWwrp/SWZsBkSxBQEl+RkCwo7mx2KUYP
+NosSC1cwEDgEmYa85ok6Kbk++DGBhDAjBgkqhkiG9w0BCRUxFgQUSzL+96khVinA
+c/v3rkVpgmeGLu4wXQYJKoZIhvcNAQkUMVAeTgBQAEsAQwBTADEAMgAgACgAZQBj
+AGQAcwBhAC0AcAAyADUANgApACAALQBrAGUAeQBwAGIAZQAgAGEAZQBzAC0AMQAy
+ADgALQBjAGIAYzAxMCEwCQYFKw4DAhoFAAQULemygmsvFuzhulUVO5CzQbDYIzYE
+CFkpGLVxDD7KAgIIAA==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEYAIBAzCCBCYGCSqGSIb3DQEHAaCCBBcEggQTMIIEDzCCApcGCSqGSIb3DQEH
+BqCCAogwggKEAgEAMIICfQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIrVmM
+qkskLmMCAggAgIICUGUvn0KkHcJIGDZGDtYe5gbUX8HB5Ug3j4GF/tOT4losFD7A
+YEQVsZnW1ywS5eemKuw/1oCsQ7YaJf0WFUTk3J26C9yQqsnQWxvPVGs8qOqHQoi9
+C9L1KTRpAvWj6envRFkk9skr5YVNm6FqShPc/LbWBf5JJCw/G4yR3zvPZtpkKaaz
+BMb7JWVRbyXuA6mbr7QaeymY8Zg602TNB3Awk8F9c/tiM6dk3/SSdZFL1aH7Fiog
+W9qDJ+3MoLKwwZcBjZxknuimNWQgjsXihcog6idwaako5a8Vzzh+9Yr2u4Cd4jWU
+TPY5DkspEpU0Se9lmjn4cKdX3AdNQNSPbn1UpBagirbM8/lfUkkbVGezUnTusy5Q
+Yc7rTTJ6WDWpilP89ZMTIEnriLvF92GhqQvb6Q6IJ1prD/iuJG8RZEZ7Rdd/phJP
+55dadyVlQb2mNOQb2zR6ikqn8F1WX+cEW609Xv/fsURblUFndhP+2DsxBbmdbrpP
+rZoboP3S5eyK53dSoH0jE+dDwLmTCRaBHRBdTHEaN7/qNefK+TaSJ7/ZhkT6QrZx
+JwM+7T0gWLTY19QSEbQDW+nmw/mT7YF9Y4AJJRjKWONZDlzz2JOuft+6gDqfivkz
+oJt0sNklvCv+fwxIxQJINIEshhiqTmCiYb//LDlskKZw/X71FE2UeekmA5zdCXhW
+ezaBLXifFYKHG3njl3IUxLSfZXz0SVmCXZb8Zvn0pOfAz5WQqIXak9nIgSMzh1cG
+OQGVeNxiKE7M1rd+Iwx7S/y8QXIVhAUeXCyOjY0wggFwBgkqhkiG9w0BBwGgggFh
+BIIBXTCCAVkwggFVBgsqhkiG9w0BDAoBAqCBtDCBsTAcBgoqhkiG9w0BDAEFMA4E
+CHluHyg/OSArAgIIAASBkAL+Y4024MWAZN/tETNU9kDN4pn7kRwdvF/tE5qWJblk
+BDoYOzUydhQgQNTxQloKxJwwQ/aYG/fmCZy9n7XF4wvSXjrIIqQ1fTiwsg5hpO/I
+NfEBjAQMDQe2PZZn0v6Z2drzagtKvP+5dcKR9Rm4hdhlT3+sTQFPMuqE/naW0RUu
+VgyufgDvrdo3eGkNV0cNcDGBjjAjBgkqhkiG9w0BCRUxFgQUSzL+96khVinAc/v3
+rkVpgmeGLu4wZwYJKoZIhvcNAQkUMVoeWABQAEsAQwBTADEAMgAgACgAZQBjAGQA
+cwBhAC0AcAAyADUANgApACAALQBrAGUAeQBwAGIAZQAgAFAAQgBFAC0AUwBIAEEA
+MQAtAFIAQwAyAC0AMQAyADgwMTAhMAkGBSsOAwIaBQAEFM1Mf/02bHn5IeF/chYj
+fTf91X+wBAhxy4p/PLmCegICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIEXgIBAzCCBCQGCSqGSIb3DQEHAaCCBBUEggQRMIIEDTCCApcGCSqGSIb3DQEH
+BqCCAogwggKEAgEAMIICfQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIgDZr
+yESnGsUCAggAgIICUI0Xi/tNudGKSqEhCSc2kdmly1P1UspJQiROTByJAGEGpiw6
+MbKJ6HhfxJ2vJvtlz+1iA2iYKA6LnFUGaU57aBFZMjbPjTs4K7gILwHpMCtv+DPx
+u4zROBGqYw5ufsvuEFQSaSeWZSwRYMPM8yVfQTZopdHSHGIhxbKFdtRMlnVOVyNh
+p+IGo0yOsELkRIxrmhdawmNsjo3ua5pSRb8nitrKIxc+LatWpp4wbwb/Z5KwaMOf
+1Xz1zKDNL4BlZVI9c3bPWM/sYWoLRxl4LF8xQK6G1lk+s9RR9n9i2r8BDZ2c2LRI
+X7KVfUhi+BWlkRzjwkO8w0pL5acoc3hYg/rvm1TCh7b2sSE1JYP5xZziOl2TSaWh
+1FNrAASb84F5XDKnA8DL+jSRevMERfP+uuWX26R3bO6D/Y/qDegq63VRSdFjSVDn
+zxwUxrb93GLSbLBJ4vW14R2Dl5WrwLzCXvP3jTTjfDVg74Av9ODE9QqEw5eABJx1
+uCUM8zhXx8NVzsRQ8U9aUOhJXK+HXVbrN4gRMZ5fqKtHeELG9mjV4guZ60cd8u4X
+fcdzyOMQzedbGhoxHl3vmcSbJb+DUgzTF+Sw85V1nqV7oPRgJgx11ib74cdAfs4f
+GSlnEY/wdP20unPGGTS5k9I7GoUFLM2bPXeue983JIEFrZXefdSXUyY8ug2UFxMl
+Ta/7aFFTmz5SBfqXUuZX48FOL1GlUvl+8o5ISbTGNvt8jwunxWJcCJCWMgQ2s82k
+PJpzqJlz8sE+4xz8qdTW1MR1HELw+v6bkRPDHH0wggFuBgkqhkiG9w0BBwGgggFf
+BIIBWzCCAVcwggFTBgsqhkiG9w0BDAoBAqCBtDCBsTAcBgoqhkiG9w0BDAEGMA4E
+CFdmR0tc6KJ+AgIIAASBkFi0RoWkd1KWd22KzsQ0u5Hl/3W8n1GgVKe1aAVx/YVo
+W+/PNJPpFZ3r9X1rx39aWMnDVa6c5wsMQ5wJ2vVzobrdEMZkvxYRXoAeyoxRRHR3
+01SFdSgqE3i6D/25ashBZWgyjHiwFd1xpK/uac9watUdcJ1JvFP5ZZ38T85E6IPF
+hnXk/GYbvedO9Wtklvbt7DGBjDAjBgkqhkiG9w0BCRUxFgQUSzL+96khVinAc/v3
+rkVpgmeGLu4wZQYJKoZIhvcNAQkUMVgeVgBQAEsAQwBTADEAMgAgACgAZQBjAGQA
+cwBhAC0AcAAyADUANgApACAALQBrAGUAeQBwAGIAZQAgAFAAQgBFAC0AUwBIAEEA
+MQAtAFIAQwAyAC0ANAAwMDEwITAJBgUrDgMCGgUABBTtLc+uuG76aVNLcrEgZ+1M
+peI07AQIkVLG3UDA77sCAggA
+-----END PKCS12-----
diff --git a/tests/files/ecdsa-p256-public.pem b/tests/files/ecdsa-p256-public.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-public.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdiq+DUTenjFcFQr7vbN9rcwuYkiR
+9kOxipGxnBFcntPNHGxt5WAENXLdmPpjhlIH90ruPGzngpMQKgNxcu8QBA==
+-----END PUBLIC KEY-----
diff --git a/tests/files/ecdsa-p256-self-signed-cert.pem b/tests/files/ecdsa-p256-self-signed-cert.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-self-signed-cert.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBiTCCAS+gAwIBAgIJAJf6ZvHS4D4AMAoGCCqGSM49BAMCMCExHzAdBgkqhkiG
+9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgwNzMx
+MDkzNTE0WjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMFkwEwYH
+KoZIzj0CAQYIKoZIzj0DAQcDQgAEdiq+DUTenjFcFQr7vbN9rcwuYkiR9kOxipGx
+nBFcntPNHGxt5WAENXLdmPpjhlIH90ruPGzngpMQKgNxcu8QBKNQME4wHQYDVR0O
+BBYEFFPacwYxY7UVPo4lqM6c72CnWEdXMB8GA1UdIwQYMBaAFFPacwYxY7UVPo4l
+qM6c72CnWEdXMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgC8D3epwl
+GR7JF9+v0grNhBeRkTfSqcIHOUR+Z9QKVqACIQDQZArMqLAZO/q5XWnCOmurIjuH
+R05sQRKTcoxBFr7nNw==
+-----END CERTIFICATE-----
diff --git a/tests/files/ecdsa-p256-unencrypted-pkcs8.pem b/tests/files/ecdsa-p256-unencrypted-pkcs8.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-unencrypted-pkcs8.pem
@@ -0,0 +1,5 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3UFxuuhLhGtIv3w5
+5GbQfOjQ84jaT9KQbEqfYQ5u/f+hRANCAAR2Kr4NRN6eMVwVCvu9s32tzC5iSJH2
+Q7GKkbGcEVye080cbG3lYAQ1ct2Y+mOGUgf3Su48bOeCkxAqA3Fy7xAE
+-----END PRIVATE KEY-----
diff --git a/tests/files/ecdsa-p256-unencrypted-trad.pem b/tests/files/ecdsa-p256-unencrypted-trad.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/ecdsa-p256-unencrypted-trad.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIN1BcbroS4RrSL98OeRm0Hzo0POI2k/SkGxKn2EObv3/oAoGCCqGSM49
+AwEHoUQDQgAEdiq+DUTenjFcFQr7vbN9rcwuYkiR9kOxipGxnBFcntPNHGxt5WAE
+NXLdmPpjhlIH90ruPGzngpMQKgNxcu8QBA==
+-----END EC PRIVATE KEY-----
diff --git a/tests/files/rsa-encrypted-pbes1.pem b/tests/files/rsa-encrypted-pbes1.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-encrypted-pbes1.pem
@@ -0,0 +1,136 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICoTAbBgkqhkiG9w0BBQMwDgQISh0OTbIn1RoCAggABIICgOWdmj2NX+yOcqMM
+1BorzrDCPbRhZHhXRT5N0JCn/+ztezF9DJNRdXODOe7Tmv4fwFwon00R3xTYgHGf
+OaN+32EWJi7YCYzBxfPbqNt/W5bnpdzg+be2T/zFsddDLSpG/SAaPQl88Njm+6xy
+XBKP6b/l6CaQIKcuujJCGO1D9znnPGzeho4EjO0usFCG1udGDBeaxTunfYXpllnz
+0+TBrtvicNw1V43vTUEmspKo4Qm64gXOfayAaAaHMIJg0s3BapzlK5mUtCFFScsa
+kFtFMOv8oy/JIfO4CRTq4tle9NRlevYrg2n/DKHhDV8MIrYrHya/zfkV7WylgqT0
+3xppLkg6xY/EWiSnZDFj6yNp2Abp2EJ+xzUcjJiVOiKlLFhZSL7k+FbMIYyRIr9l
+NQfTBHERqxdhj9OsoUMJnvXjrk/55mCkifo3DqbsqlnYlij44GgUPpKupGv/k+Bs
+Sb0nCOgvBqUv1IoZ5W19rc6LHcvV2XOObJErEJFqLwgHRPbM6F5sNqXymBsYn/Tw
+N/ZlJsWuj3R7eExb2vDvTSONoirIJA3Hqjytr6r0hLi7lxvTwJ753Mp4uLf0vD1d
+6GzD7Un6CIb8eEXYjPP3GatpJDt2Zmx+5FzyRtk0V2EBY8xOCW+Ib9acf86oNGnV
+EELuDOU/IZPzhq3TUs+D5R/etpaAEcXt4iIzDmVaouOzQE1IO9zDGK9fds/9Wyi6
+PRwmJ2WdAE0LK1blHxl2S5IGMY7Sq0ucoEp724X1C2E+HqfeLmQYcpVoJUeiVKzU
+2HfYNjmoDLsDEDd+Vc1il1pbXsv1wNdoGjolosK0RFySeuKM75NXrweezoBGKYAx
+yj4Wp+4=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICoTAbBgkqhkiG9w0BBQowDgQI4xTr6w09iQUCAggABIICgBSW3zoFMT+RLN2F
+OH48irzSbCwYuOAxHxBB2uNsAAalagqIyq++/X3Ww9wtMJU5NRlFu2RZ2MGwV6HX
+C5nOHIXUlg71b5XTUp0RnP+Qi3TP+hAyQhAEHVIdnEUOLIPp9j5SJs+C7NUQUsNr
+UQExWN/72nBv8wSe0pbKJzCk1ogYMechYXAo6jWnbbvgUheUPG9XJhtl5mW5J1/R
+uJJP8vrp5TEsVY+8BRkCulOlOaT7TywCwhSoDJG2+nA06wsEhEY7hubLzSvaircl
+TD63N+CQhTCaIMoVxovG38cI1+fuADCgvKWlMtyhX54cDem/rzk5N9iKtVAqiLP2
+cA3w0nkcOBH++FvZLh9nV+yrV60nkhfl6LN+3Cca8qcwyzWgEg3MCcSBGzcPpozC
+U51Yfd/0jv4yjCCJFh9BKa/GDaEPSWrSfQXEFkbTXlD2e/G9FTMzyYmvU6rK+sVa
+j3wrCqemBMW1xuGFk1b9UntZ2XgMPShPDlGsgwnegWv90bO1VRsUd+zyUbuggnT0
+ZAbFKhPVbkhvlW233MvteRguwQTCL1DlYPo1i2HSc2vmozcxJHPbriEVRJ2QBznU
+6Wjz1JGwKjJeNEFDT0jykL6SZNu8tr9VIUxdxkxxIipAj2Z/EDi07TiWJ7L/SsTe
+5quEVtjdAxfMBTSfWyF3NRzskypazq8D7y43LMkF88cGjiLVw19WuEd+WWVyhthc
+/NoALEQb8VaK9wr5U7kXMehOX6pFSMn9s9P4h4Ch25mAGXhv/h0pw7HBZAv1a4o9
+3TpghC01SfDCpo7raXui70vMHG4TSFqJa2Odd8apTBReMWRe+U40Q6rxGlw34gk/
+J0JqYno=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICnTAcBgoqhkiG9w0BDAEBMA4ECM6WxHwQYHWoAgIIAASCAnsARgLVpWUcxsM8
+i8xKdB0QCGKTBl67KKFwz3gdix/G/g12KxpGFIYb2yv0kE6D9iYBkrQhpdEuLNez
+K6E5bmyEJRyHVRy0QdmfOca4pq41Q9kq4+6PRvDR7OhtbxxGvsAiFnQYv6BDXMmN
+iRwDQ8o6R7yjBHTiY+5O36nihjikcurRmHJ3Qv89ZnHw4tg6giTMMVW4wQeuLF6N
+BKF0jntk2Jbfd0WGlWfp+WFxd6+uLeYa4hN9F5JEnhqVDTbbkWsl9pxIZAEkHY4E
+20c45SoVDx6TizIj0xENFnXAj8WlEDepSptzS49IGHTo62FYvndL23Rq3tCBs5hq
+uhbWcDrIJIggFSyyXB0tLFZmPt9fN+Iu2CeoAleeTPiI53dJUsBM7suZ40R7jae8
+9XXEPXcFt+n8iImScR20deyzjr+BJ1oZY1Nr68X4tjkL9cBIR7JvhrMN+lWqhE3P
+nwBhS2uW5UCH5iTM+dBjFiML4g8FdVPBk4U/G+opDYFp2SVrrVZx0bhPAk+kzqdx
+q9x9W5dIl+Ck+rC7jUKqmvY84G0QxsQxe/ZZAL18LYOCGi56GkVMbHdOeq1c3nac
+TdjLYXRyEKDS4XcwyFj329efiYepvXlrhXbg1Vt2VZFYG8ZU2ySuQ4dvC3pWwWH6
+rvwTQQaydSe3XMETSGF++fBO8KE3T09ud/wk4oCz/hVRxBD8AoqEzI3HRCF4u3WE
+LnI4SioONlYdNJlKQ5mX849DzTzTQxH1+J++eYr2Hc02Fwfr7GoapIStvx5mE7v0
+VNFxZ1PJt9epHsRWl3+B7qc00uYepLLApOaO+R4LA/edP+gYqDh7ZfweeCZqXfIu
+7w==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICnTAcBgoqhkiG9w0BDAECMA4ECBLMiGMYBQ5qAgIIAASCAnvGBATmzbWlvdAs
+0x8+aHrCo5HkzCFwDQkCrvaVn3HZXx09DqK2p4TANA+iaKn2eQwqljuTuXa7cCPh
+t5ZZSxDnErrXalQrXhAtzTX67tHYkyeaIBOWdvIzRBjQKFUfaN0zS22Itkx0ZTc2
+RYWsBXYNe3JwfXP+0RRv7Oyd/Kf1TgINgjr4Lx+T+PykuSh/CAvg7RKJEEpyXJ3a
+11UKPuJpK9OeXMGrozpIVTZ4n/jHH8+CtEyZALbh4YbSv10dnXF0UnckKmG9NOya
+zqX5Um4gKpsgruucrf0aWPrfw0dmKXVEKNHInAWiknswKuG2VgRLoBQ8yoNqGh2y
+7btNB67bFSZsTsM/Phhc0UbSGLrsidKV8tnBy/5m2fE2oUTGVkMChVMbKfHtAzey
+98fy3t1CikPW+pq+2I823X4UXAHsffHHey6Q0Vyc3t1klPcbhCP2GHVBBE7D8lJs
+/qS0vxoMJGqAvwpNd/9dPmh1aY4jIBufS4YOuwudT2CpidM01a/sOcZ/u2VYFral
+OrzYFEVqD+j5A+/g9qMPiUYFduWavCJJJjpyILuDMSCCUkPHkLVTrcieehieiT1t
+XXUq8EEQQc3pU7l55SUJtDFGj/E7r0BMBXv6WchowzbmRKTw3XM97FlHQBCjAlzy
+ls+OPQcYfs6Cwo0McKjPSz9RV/G3rb7ROGthsh670OZ+8zD2QjF2rdV2evkor4Oi
+tM78TfyLqoljmzi+nWR5tJtqIu1aapqrgLmYTKCju5G23gPEhNpJiCZPFtVDgRJi
+Jnr2EEDIr0Ya4GTjFGuhvMVBxJikKivJtuwxFQv5GJjZigaQ9OUysng0xi2ehuq2
+9Q==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICojAcBgoqhkiG9w0BDAEDMA4ECODLLH2C0BS+AgIIAASCAoAEF7rmVky5iuWg
+DWTT8y8r4TXJ861WQVo2wafDh3CMqIdFINTEHotenQf8lR8OOTwpQgvnYefO9vzS
+vs5bYYfx56Teb2kl7UDUIpcTyHoAKDqQ30G/b0Wp9xuX9XHxx3dX/ulKasjhDk/1
+yeMrj9elZ25/Q+zpmwcMcY0eht/9JjA/VwWmqqjys+QxypfNtfM60yffYKhTZl2R
+/Y6FNSxAN+zIwl/xqxITSXuqnz2XqR3IMthS+ZWsiAgGaAvh6Fvb+43k1zraGOdv
+I5axvmDJV/nNkq74VEakFlDBEckSgNOf4d2dcmiBt0vy4ASdNC7Tv+3hQlmmXN4Y
+tWMaY4pgPQOThvFlG/5ukSmEKBYMxIZzWUNDZS870Z9ceNmbp+UnyGD8YkjbmmQL
+1WGT3TF/33NdHpLy3MRy0ASTxzYfvYTRgkWnfpzGR5gsOVu2D7UeXbjzcJPkTHZK
+k8ZiQ8v6ACtDxCx2OupGbOVcAywnLP9u8wQYDUPLTq/TdeoBm1W46HmDqQwA/VPL
+YIbl0p44ha5plhtxIWN+fan/asGRBBf2pHCF8dBgCGj0X77fYSeuq5o7fYn5wVUx
+KaLR0g4F13ji1LHzuzNF11WA79msHpVUNIK3YTKFk7l6Rup0fSRSGJZUSQg/TbP7
+O9rwkEcliugCM8BGUibs5ZcQfpHELQNA5QHgVWBC2Lp39uP4OSjKDdX4MsKuPhBi
+ii1e/xDrSKoVrwJjnb/elKoaFNIczMBn4PD82qI2sARCfLC/zju6WLnfEeA7F8FT
+zhVZ0L08/I/EmWZueDok/UTwoR96l5ED4TUhl/f1PdChw//wxXqTmnf8FM9x9u6j
+ussjx7C0
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICojAcBgoqhkiG9w0BDAEEMA4ECBavQC1n+uBBAgIIAASCAoAcCG+qBemh0n12
+3dgUZr9MN3TzGYIr6qeL0puLn61FAmRj8kQzvIzCgL5WOUtnhzktkWZO0oHkz6Wy
+3MU+AMi3pjnFJ9dod7/UBbnjsoEZ89gqXc1CkvtKcgETOgGxuueHT8Xi65PwS8ss
+1qiks/YX4FinIrFk97VOVR3/jd4gH+bMK2cMaMWy7GLUIVgEGYuaa1Fe/lIPkpyb
+07OeBaJ6bkhRf5dhTl8CIkKhsW1vLUkbVN7c+JrBeQfIw2C9qEc3DrepEXFa5Zj+
+Cub9EJATUdYmCXq/bc05l7dVrUmZWkSh6VKJSuC8XlCJaYMCW/ok2JwQzPmB/m92
+uWOQtjxhyKZ7BdKEAFppNxPQA2+rgQYVBQ1erJemAw75fQF+izU47LCr+u80jgWD
+TSdxMxxuOjoFQaDu+1/j6gZBcK8ktYNn/wUPZYfir90pgmAu6S6yoM7gSn7A2X6f
+Gqg25zPJLAY7CRWLBPRcHPUau8S6M4mH0Y8/ohhcOpcUraunIrB029eWzm0ll62S
+TxL40775YOLZ6+1K/BtW9ZAcTodLb0j5jBVAMujxKWFKNz4sgs4KmxkjmFEo5FjF
+6QvX2OGg0HLwJtEtb8Q8B2L0z661jNysqbP8n+thZfNvgzYGGl1Qfuw5sdpnunQv
+rJygKIamLhl6vCCA7BAyzB7cjKWxPYFoNzM2yEVJ8/eAtrYowSisGt8QJQJhO6aI
+IICAgD519C1KLIhRuUP71SCudUZRSGlaVIiAwOv3Gx0cxDysDIdF/hwZfHoJWt96
+xoqgp1isV9eac0dzVKZ3rQFoDBjQ2j5wCAB6/MZP0Vgv+Ipx+uhrBOmvCMzdxh2F
+vf21uloR
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICojAcBgoqhkiG9w0BDAEFMA4ECHtGUce8opaaAgIIAASCAoBqigpolvtZWgII
+AMSQNceoXlu2Lz0RJaCEouVe73ACMwcnDqrGD8vEt88+sJs+hgee2wqjXJKKVmyO
+KuA9wtl09z7mCfcPAmGe11P6PZ7N4rdxYGR7URqQPsD7iD+S3v3CLlSnYCJnY6Np
+x/j4Cfc2OIH90Vb8gxT5xUbF3Fnt459yyDnOxJoxSxD7pIv2SJqw/TD2gxPO/j6B
+mdbKTMubL0laDraoBLQi35GMpd9Z9HkyNKCxuk1Bw1ApUmXaaf8dWOxkk/5LIzge
+24eCOp445zoktbpN+Te+osHan7ZILte0bf6r2D2zAS9gFgx/xJHDeLcCYmbM8FCD
+msygHM/rqllpj9ztjwbpsnUVpLGb0WP+mx7XWGDxRG6aOlStFw1z5vC3cvBqG6Vc
+T0WW+xWcVxMHzVjQXkb2K7RUiVTq9DXQmq5cs6VA14InkulMXmGtaqFO8jrPOXO8
+vdauxVRrjpzZFuGprCN3gK0/rpZ6rk0i0pPJ3PRKw7OXsnHXSwzGljtP0RbBGpBq
+zq+5DIgfhLVHe62TFbqCKguvZIY+8To/tm+1ncu/unt4xhLZp3lnXZ8DeDUfwmps
+e6RT8hXIEeibFy7/nG1lnt3KfMgKlLA+F1nDZyv+s6T2/IWOn5i0z4jwkfSQr3ND
+e8b3AYukHZwgqae7mYw5T/SnMo19A2A4Hj7qmKVzzK5p+P5mOU6s8CEdQcgsDvBG
+geAArOaRkSwL+n/AA/1gjuHbmkF++hRhlto47rUui+mRWU0O9CeUExks1cL7wqEG
+VVB4JfLBXyqZpjNyIqfiv5TXqhY84yOwuIXd5K6FUCRjNa1w/NijwBk3qZ6viCNM
+qja7N3Wj
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIICojAcBgoqhkiG9w0BDAEGMA4ECPDymiJgHpERAgIIAASCAoCu4qQpIS8wXbgn
+eaJB169bcoog3uWtwDKOrmfH38M7aVwOW3WEK7vvv9a1MCarWXUFVqh44WAKppx+
+t5ZQto49prXEHu2DhJn19Cj6bG7nd2E2OHmnzGjwqgm6CIJTjFvPNRQZ3eS1dymO
+kM8sDBo4Sq6AhMSg0Y8S8PselzVqklLFXzXJGzJDSLpvpxoVNsqqXGq12sKvQbcZ
+JTAsydHG9OJhOvlku/FfFQvBQY7tw58+UhuoxDtLVV78zhFJ8dYswgBZpYMFbP27
+HKJtsv/x1Jg+Rv3l2AN5kgoUJ4oH8Ow85PzaDBiojAtk1/mENzBG9V6HHm9rrCnP
+QeX9G4gFuyIfV6MJa8D+D8DeE9tBMWeD9j2d3zsibBDfslPR6JnjrIeDPAvZhgMC
+wzNkJ0Lrbz3j1l72GbDDdp45a5nVQYXpyOPYfCv+EgyUC6wHIUZiFnpae5g/184y
+vAMkiv8/7vXK+/DqXmu80yMb/EiVrmmrn5q3CmTfuNiq/NnlcADXcYOieFG8P8rE
+6rGoyD8JXJcZEG51aOrE4xCVWRP8SvASuaYkJmku79wRlh3hnuRjsj30pkWYPCtA
+P3MKaoagh1NdDHflsmap7U5m0A7gGZ49/c6X0vk7H+PKBsQ+97UTza6oCVTY1vOF
+/S+GdwtsOxeQlMBtY2UGa7b5RUzZUVV+HgM/0Wy5ry5Yk3igJxh3+7pp4pzxCgjK
+OlATWZnbeG8M5V5VGNfztckYm9VouW75YatIhFhN2tlB5zI4cnlns4PD5SBxdKGf
+YC7qdy+5AZ1aOv2AfD60jz6asSeFGq1RQHOubprTOMUTTmVDv4Li9zcuS1Zd37Vy
+kGbbsAyZ
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/rsa-encrypted-pbkdf2.pem b/tests/files/rsa-encrypted-pbkdf2.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-encrypted-pbkdf2.pem
@@ -0,0 +1,126 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC0TBLBgkqhkiG9w0BBQ0wPjApBgkqhkiG9w0BBQwwHAQIYhESCE5rdlgCAggA
+MAwGCCqGSIb3DQIJBQAwEQYFKw4DAgcECBJVswj5JKmLBIICgPNcarPhUgPzqAFF
+xb8LvwWSW6ES0QTM4d+/JydSSoBX/UHW95efGhrUOVqRTuuxyPvlEepMBsqHwfur
+C3IlOl3LayZ+vkbbkevt0fbb/pYxlDkUqefa7q9L+VJ52KyWRhrSytn/nvoP4Ptz
+zlYf9C15dPBg8oKHYxrKThhN55kJXoJvJk+kCCuun2sGECePhy4gRpAhxKYiI68G
+oSWUe9PisHdO+VbEbF310JlRWPHq22kYfjN8rEkNVBvW2WSMLFkMfTGwAxQStz8A
+n9sxmIKMuB9qXKin0XlnekByl/ZZdXdxIYmZw28627L3Y5SZM0YwvHDY4ZHbIhXB
+1iiFAabQBSw/1HMn3Pf5wSikBUmfRPZ669h6F1+B4yDh9pF6qoz2bZR1f+1n3Hfh
+BYlMq7icwhJukJOJX+GnILl5JkqLkM4kUufZGoBbcfaB2WC85nAgVEggNj40H6E1
+1HqiKTNu38FWnUsF4mez2urGS3R+o6bCXyOOEoLwfdqNCSY2PVMBRYyTMtkfj7Do
+daC/zQBlal5+GrqiyJfgHAUJA0MkZHg1wB01nRSXadSjhMqbn4FVxDbd1ZjGlCgv
+DggNqs5u6J6vg6CXnu4SN4MGIAJS8l3epbYIKmu3//+0ujD7Iy60HPeReABj2Tnj
+E6ehImW9RdbAo6dZBOO2m+DHpdo9KwjHakNZ9Tg8vTj9I4Qw7wUVFHZ8mYxe1IKx
+mi5KH2LvOdcaYZJobWDvGip8hjzF94MZrH/ZOXBULfgQd5nL2mzex54q6BvGD3U9
+GpbPDeOcUZsXiqxaA4RufI92YKX88PX14XanF/J+Zq3Fk8qbEzTcOebH5YHm3YPP
+Y46cGxA=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC1DBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIyjzOM682NvICAggA
+MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPX4T6nk9ZThBIICgEhpSFw+iHX3
+d1i1P+Nr6clbNmpjcOGjpoav0IgZDcFQd+bKnOAAR4IEdJVXCUGOdu0Sb1wDTsR1
+W3cr//6V3HeiaDbzSDi910S8IgYyuYhnFAFdw/FHoaO5K67+vhaedhV3Vhhio7xv
+ZNWsaiH86ulFpc9GNOxc+DQK0CYd5KcmJA4wMVu4zEZ6uk0k5bV99UueHBBK7mBB
+my7QmpvLpSZi0v3Q9oWP6d9mGyixsmER83vmnCr92cmq9HU95OZl5Ach6WsdDn5E
+4SeyfZMS62/xqIXf5qyYvPgJ3vyzxIGJDB1v8lboQ4YZrcSKWZ3RyKDx0Wsj9fgA
+JJxWYtj9Ukglz4md6Q7kuKpd4lwL48Xp7/voliri0Y5NsfFBOqG9KwxcC/bQe0H0
+vcvDrLZOiC6J/IGSxterbl2etVRrzeUl9HGJhUyHpjMLUw2UmA3PUHnCZH6dPMdK
+S12JpEqVq+7mkYXJH1Y0eAR0Nc68exYnB69SCT8yBQ0Z82FvhY/NwGVgfDMMAO2d
+rieVuMf50hztcDJ9sYTnfQNtf+0mPn3b1SqT6bgAuPBcumS+RkqNSJkmriQWFsjF
+yDIwpXgEh+k/HJ9rhMJP9dpoLbHjQFEi1AYW1pX4Gyt0H9YkwZXLiPO4ymDttLBo
+z9DqDX3bbbbPopNuA7a23LxE9yzj7BfZhfh/kiqCpvfE9R2H+pcPJNraJY9A7lLt
+mEJmCGYRg+lA7uCSTbuuQkuK9wgz4oD3U89CVuEmh7+m8SK02KDxAgk9dONcAIaE
+FhUIBTVhQqhTwxoEWrC5XSarURpJY8iwe6PckfwLUgEAqGwADHKF+wwyCgCgw0sA
+HaOr66k6iWk=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC1TBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQI0IWpzTmYNqcCAggA
+MAwGCCqGSIb3DQIJBQAwFQYJKoZIhvZ9B0IKBAikzaHZjD8DlwSCAoC8h8+cuolP
+hfAj9NJcfnQHpKoMZiTeTpu2Qu2zYuzkfwjUSjJ76lK3M1gj15gllieTGVM1BUTC
+nB0isUuIN81Zhuj3Gja2iz+y/BEX/CCfuRXkl/sv3Z5USk6RHPLlJOA5HwNNDTnx
+CSXlZHqBXIQdqWM2qq3ADC9IM/JsC5QkA05rStL151wep9jOgNJMn6F6NzOWpd89
+DJfVAyiUlSnjySHIVjlBtTkmrBz7pF7uNnwIIXPTFx21i4RjDuuRLF1DHvGkO6Yj
++ftnpGOPRBoDuyfSVyWkiPzd3N4Hyuw/4LBGrODYJ+QggrCiFgebZ+W4K2NNAAD5
+LMWsnsd+zXYXl702z0PwRdzHg2R/Qk3MHq+0tiqxcVsRJ7ZtbXDHZT27yMe6NclO
+5WYXgQjWIYddThf5cCzkud3uee0kgAnYHN5FWX1JsGGG+3GtsClLw2HcyrcFBDeG
+KcAVrJjX4sLO/JFr5awOY6WGyopKC84MM144nqhSirhotv7s3DoVywkG7aHuggqA
+On+G+FE2vBxlxAQImuX1TZhvSXo/OTO//Id9NBInb8hMrbsOL6esztuXcdmTO0S/
+DcjrrCApow9TtFvtCCscwesDnljSM60MP4kMldzJAUOMgZPwD3gWzF9OnSVl3Slg
+GZk9OFXXscQVhhl+hNQ2rD0ea0N2dd08oPwS1fChv8uYJEju2D3x7GqK0hSceps2
+OnOgClrTby0wY5CvOwP6dlBA7KGNuqzp/zGmGciXDJ86TiSO8/L/cJzkEhYyOidX
+nPY5eGpf2LdMdBTkRvQxqFA4AUEh0qnCe4jyMkaGenAg+B/w9IIjSX8WBsmKKQvW
+xO58a7/TqwVZ
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC3zBZBgkqhkiG9w0BBQ0wTDApBgkqhkiG9w0BBQwwHAQIbkqrr8tMrPoCAggA
+MAwGCCqGSIb3DQIJBQAwHwYLKoMIjJpLPQEBAQIEEHN1IWKxGrmHi027/+3DgUEE
+ggKAnK7ietFdSKlL38/wpgt6wT9Ef/xtpbFL+y835tgJc1WQR5WDdCcuclJunqcU
+H3wJF84VXiNg/jpLj47iDtGU65X5iF4Aj9KWppU4IVkz7XbLclXwBGTZMN6i4Uvx
+DzqeiDPGk36J5HpqntfmwrPLGQi+ua+vfWEoFQWOBweTxF9ROLIFtcZqF6Vs+rpp
+gOvsqDFVYJG9WUDFwYkF8fLbH69paGw6lxHTblXiACNYPx8uesBJ42LHKxuivEeW
+dTwqUw/VYfo20Xwih35q6LBo8eCA/NYzVUjGwcH9X3aeSRgEfN384X7HO2Vb2OcA
+v5XxE//F4IH7iOBwnW6MpIU6yQkwp5ZnT07hZ1XVTu0r8NZbRoTK2slKVdRUO5ND
+/RuO1JhOU+xzGZb0tUgZm8ns+pjES8HN7yg617ZrxX92e4bBICc7Ntp4IrQdVL0A
+lqkeyxjFKkjjPJvM19u4I18DDPprdeJcYTRRa9JHRC4v2lprLeKT0euA+ph4I+HN
+pGhKwzhTsVVt+L7DYGxoA+SxsVvMPc4aCI02GRbHuwbzk00Z13DKegu76C1g3ZXl
+JRNZUswwNMzJvkaTXyAOEXIEDGft2TesaRtHvJFKmfokd3bM/a14xYvd3CpttrfR
+iJDzXAyRtsd1iQEvQu29dkLdd5lfIfO5AaiTPfj+7WRH5QpwrBp2yTXi7zWSV7GO
+BwAujbHcfU1H2vNTvPcBWjebWfaACDcLrxqW4Nye6+hxg9fza9+JqTMP4FOwkf9d
+/Y97X5I1X1ryDrVb3BqGbkPjDIo7g+ge61nOP44jPMYXzcu+/OftUfZeToVvAtoI
+I4NBsV5UiN260PlkDbdhoMhkiQ==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC3DBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQI81JVAGsb74cCAggA
+AgEQMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBOgQIZaJ0tVV5iKIEggKA
+MC8pz7F7obdiPp5xZG8ygIO/G5rV85eRjK/DHnY6fU3mJ21B4oSz8LHE9U1APpCn
++AlnYvILP0IYHrUGG21ZaWvO8UzmxefXIsroBFNdO6Q/8Yft4m0PsVKCmiMW+Rpz
+5ejsoqxJoBRIHi0GP9uhf4R8UKzLw19Qb1jwjEIYmBLD7OEgn7trwIT1ZwF4TBML
+aUKlm/Fixri85ePq2eRMYWn/Y30lI7MYyp5Cuh9ynK0hhBUc3pvvE4dmSgLlZUUJ
+g2IC+U2MKepW8bHUMdUXxaKFva4/qB6vrhtKOiiE8MAK++OC4IhKtl2dNN9nTLqF
+nm1Vhk/DLqVs4hnnQuq38ixl3mCjG8JKFJKJMeknfTY9c8mqf2s1RhIoSX5jM7GP
+PehE4AGzRHxg/x6OIVQd2grwJwJY60uf4eapKHbNlUDeaAZ5LYyqveHV9uzPJy8g
+t4hKYFCplyZfVsrtVjwGIUABxEN/l24HHwRwIulzAA9MuLMBsiU98uK9pXQk9Zkl
+yaOkFh1G4GrlkOkkPFh6MbNjw6gZBF6t69rQROtNSxtm7hDyl/kZ0tgtU948B4q2
+oCxfT7LicHCn/ms9g4xTl3iOn5Xrw/uXjNKNgRKtyaQ/6NZQ52JqMczzHsXRyZ44
+LioKsKlBD4kyMZkNmvRI3Uz/xbM4/lm/34+ntLkAJ6SeUr8ui/XznjD37VOco7oJ
+LMhQZwmzP+GNrK/xBTcw8PpQ+Iz9fyfxYizZ9GyZGAaBHXslupANad9dXSCefOsJ
+PFRkbiCFmfkDMDeZPdodAnvV+5u7fQvI5GKwXop/M3rLXdtr2l+zAUp/LLn7Sg7d
+I9AhucG2q8VGflF3MRVUrA==
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC3TBXBgkqhkiG9w0BBQ0wSjAsBgkqhkiG9w0BBQwwHwQIQq9ZzQ+fElUCAggA
+AgEFMAwGCCqGSIb3DQIJBQAwGgYIKoZIhvcNAwIwDgICAKAECL861DXfO+9xBIIC
+gKqP9tLx2sQmVIGdj9SPeI7jfY8XtYUAg3UBY8QSM2zeN+TL3cb/kEdWGsVDjUGz
+meOwB4ap/pQjR2OsfBktztwhmaM2g+ykecxm3Q9X94Ykfs4lbXFDwueAJ0BGGEsx
+/TLq3V3DGDXwfU2R6wW0eoY6cFNZfpmpzLCX9I24OzZG6kWOQU7wgTzN42SVJCnd
+bkmclJd1ftgZa7weJA0rI8m4WV24WUY7q//wki9m0f7FMh/xrKcjqV3tdJ1bCO2g
+Y535Bbj6E7iqvh3Zn9kX+jKTkuUAPr/dY3t2p+eGp9xHJtB7Gt+Qcf2L0q3eerd+
+Oh0kV2ww+W8w+ukNAtGaW2La1SmJCbWKE+xgX4kyFDrMvlHjSJcVz5wgm4UarXet
+vfnlKSvcs6Cm2cLC+Wbl4jcsGjvgAA6Cnp3zhfmVN30eHviZuAwFrr4bOn/EMQgy
+l9FNAvCRvK5RpuBtu0nZh59zSD/aZ6OXmBOgpuYIKmBLrRnav3NjGpl2sP0M7HEg
+ruZEgfaqt9xkm264TfA8nmux/D5pE0OayeoO0RiSvmgJlHlr2wLjh5CqCXaa4Qx0
+afyN4H+WGngTm10EdA6SkEz01gLPfK2XNZzM/tWWEFS77kNruQ0XU26mdMxplUzS
+oF2Xi4SpY/2nIz+ZJChTF4NkLk+VRrZr2qp0J/ig/LGy7ScPDrzZYWql/2+mWqXy
+ai8DFt5wsuuJHohUvidA/sVdm6z/S5qqUJjoxwA20mYontx1lvYo2eVS8XJJhPpd
+aCM24EQdCkcZ/vEBakEmCWMjoDUfPFC4hdQ7rHichpJSapoCuMgaP0AbN+5FTFCX
+Tx/WJpHtdpYyx013dr5GvYU=
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC3DBWBgkqhkiG9w0BBQ0wSTAsBgkqhkiG9w0BBQwwHwQIGxaTRI/wuwACAggA
+AgEIMAwGCCqGSIb3DQIJBQAwGQYIKoZIhvcNAwIwDQIBeAQISj208H4LOA0EggKA
+kL783AgZq5s8Wa+MG9nlbq4Puc6EhMzW0Nm/EcMl+eydC4UZRiS6gVT1Bn3H6Ayy
+g4EcOsFQF/iHLgs1hlQTant/jWtAxlporfUzxddNReqrWUje1xl6lW4Kd1vPwcuG
+CU5RWphp6XsEiJWV8f3d+j99c5Jj2BRKiSPmS+CHeGQlVaSABkDqOixFjQf8rOwP
+iwTXppqnGbOEkhzRvR0O/K1S+/5ugw4OumzysjPI35hZuk8S5Dp2kpT+ywVqJ0UC
+uZOL4kykmu5OL+Yhx4SNlME8jlG2nZnf/VvPAe6Yq7RtwwxVwTPL4LtoEmUQWept
+MUu7XdA29KghHGrWlPoVGrbfnq+1P9xHlYQs4ZYQU5MTqXvqaIO+Ewbxkjhn31jZ
+17AWfNKiAym7fVQeM8epJt0SHx92m3MY9ssdbfnrkXFPm5zk0d81LP3Tz7Pcu1Sf
+ejAmOAHmfl7Bd+23k2VH0QqKVEKanNJX2F7KrK4YOCz4+jritxJIAoro+1DgpnWR
+kEe1UgguiRcsL0ZPXjf81ch6OKaWR/yUkanRdANx4+0g/lyrSFQtY3XRwFbBRvpV
+O1mvs0OQRZZBPyH08H92lRdoNolEDbcqN337mUZzEqy9e5w+kC8qoZC2t0P5BDcw
+bYDORuJRIldYCafK46HUceyenv/1trObRFxD4fZU+GzZaXAfMqCrjRBfRUgJ2kPy
+QEW9rSqBbqZhQmEg2jJNtVY/GnzvY9F534r0kal3mEA33pCmPf61CPvlSuZB7owN
+9J8DSvft0sN+8BkaGCBEwKS6szjE8P92ezD1rH6Qo6N2p3JGzmg6GwAvVYX4vqVk
+RGdqREJ8fe0tz9JmPaVl/Q==
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/rsa-encrypted-scrypt.pem b/tests/files/rsa-encrypted-scrypt.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-encrypted-scrypt.pem
@@ -0,0 +1,54 @@
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIzVHEuZIqTxYCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBAgQQdFzY/Ib4OkmYCNavZTrqQwSCAoCMD9qTDZO5
+328dyE3+t5FuiUj2ei6u6ibPTa+o2q2WtwnnLSzgRXegAxyy++X0JQK+8Ky3XLFb
+hjXLTlvNm2mGgAgjcuvEAZQvkB5/oB5epYqk78Nup7EQUcLiG6mV81E4EJ2CDaJI
+qdsHUfo7RLnjF4AIu51OZD/hngEYxFGFPgxwqgPW+5vbEbNwMVcQarOLt7qWCmMR
+RSe8EJBFJNi7zwxgdM9+zg+1BSiBvo1vERinIhjlHz+YOc1Squ40dDIC1VZkL2/V
+BvGWKfuE8/niVgoT7uPBIhlg/03Binn1GexmrNRLV+bwZsVrkmXxwNBLt6XIQrAR
+ciPmCnRI8DRA2xMpQrKYHYdIE0zcA9381tsLy6MWd1fN/5AOJsdJyL1ssY//BfNU
+eUkhah80sQrJ54HUIkvX1SGY0z/+Xc641LOlPLZb7p+t2gHYAkuY1GIRthFrAhDN
+SKkU/P5R14XZgILyTDxcpTfZDiQsil6Pw1WD1M4W8A8tLFpaGqTGN6wtifM7ZpNH
+JPvZdkOBB1EB6c0lKbUQ3Qg7tYBlPoUl0n6IIGk3HuYxmruMiojugXLkwAFPA2K+
+vPFIiLHa6hWy0YpVv3A5Ta5fiDSo+YMWPTEeiuiry+UVP49Y8voS5WEmuauNORXh
+ZI2pnJqWvB4GhlUEcERYNgpWZt+Z6L+Saq3NR/u4NA1mO27DAJ15adTimg+r9Ipl
+5gg+ArChpsDbnr372Wx8kZgnJDGS9H/0BuhOPvhIK+sic1gjDRvMhums4+sPas9T
+128lfP3eaiHaFnbuzJEyKe7y9hNcktP11d8P0tQOEXmz2cyRtYj3XenaDyRGh65u
+AvCqLxjvemyO
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQIXyMMZMH+v6ACAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBFgQQ9/sPlmVc8PwQ7qCji74yfwSCAoClNM8rzEcQ
+fSZ1xQHUTAyKBhHD+aCz1kZgsCQXGRlpIgbpoI6HorSRXj/piQFoT/lvAlbGoy6D
+n/7b8a8IS7jUhpfywS3vt6/J+d7LhYZ7pUOEKyoor4d6csUKFJE0D3hF8E4/hCmW
+Zor9fYTAFQcwAwNtfocShkWCfFz8+jPluDpdjAsuPltRUZfM3fTPH/L7inDmEnuc
+G+nwMMp0w2EQKNNTj+HvPgWXcHcIPYHd10TFJUO9n2JZsSHZ/6YuByudtAlhEBcM
+AvysRJyqDTV30Xi07EG1az14tlm09dO9fObpX4v7jppREiHUXd/ypWCwGVMTyat+
+UnOfKbjRFy3G1qQNj0K6TkI+4EumP6+oSB/YEiLoN60b1ZbagbAPyhbarlmJuSyN
+MJbq9YLkofDJQ3aktHpoI2+VDV9o9dqxKsktEEpJPcwKd2W8WGneObACs52LTKNQ
+e6uGZhs4BdRbNlw53p9YdhIye7W4B4NNW18S3ZpIacCMqZJ2D48HFwWQzmTCaipG
+tswpQIyD2Jb9NSBrie8Fa+P82RuLuVV8qgJYM38Mtd+dAEavdvagrNoKcL7teqSu
+CBjP2GTcTrciJXoS0DDakFQMe2z5X1UD/qmDEJV1RmRRSCxYGeV2bT53JyFNQEAZ
+ZWsZRZ8jFCBVjV/LB41/0+Zbqxj7OmTOxxL3g3hyZ4E+N2ACIUZJxH9It0PNQ3Kz
+Kik7fuCafPe/oI4Zb9RRjbW6MS3Ku4J/DDPO193xTqfi/faMhmplAhuVDv0z+hlD
+aZmr4VFFsQJWou0mCecIaY4K1JVS5QlIw92DHUB4C0BXH0SOYLbuiCd9m4t4eZXu
+DPkUa2DBSaSi
+-----END ENCRYPTED PRIVATE KEY-----
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIC1TBPBgkqhkiG9w0BBQ0wQjAhBgkrBgEEAdpHBAswFAQId0JVgBVSslsCAkAA
+AgEIAgEBMB0GCWCGSAFlAwQBKgQQlJjxBqYK7Pwam8z2xY71LwSCAoBdu+1EInkY
+gLgAE/Kj052Uhmt4VfRgSKhH7mdyrqwO/sJ1hprIgndjoIdNxKDJIYn2GBOJ6eh8
+134NL1HTyicnP3umstphxawzl4EbHQLtRjIlde1sEgJR47BeEE8evjB2EWD3cUbT
+qbPK1vkl2/KKO2muUEbKbn89ASXikRBXasT6EwY4mL+7LZzuqkrkGRZg2xXSPtYp
+NVxcL5JQMtyC/gbJJ+WH2PvgZVIGupiIdUoVcWmY1rm6Z1zyj+fuUF8Q5uOZN4BR
+ic5to3X/wHSEqzApYz6wo1Tioy0OPUnlr2WODKGZ+mJK9N8YetSqQ8HzkGaZ+O+1
+G8QsTYmitHZNdNK9o0xZJF1nbly50+AqOEF28/nzfcgmE3+qQWJW/lD4OMisbyvF
+VLF0EEFGtpg9+zNP6edq8zQMqvOM59NHfpy8eh2RemNwIVvupM2xiuS/xAztbJXs
+21cQP51GLE2deaoDn/nmpa5cLOfIvO6Ni9Sv6Rp1yIGAl1hBZZEpR9GhCXg19wAa
+lUFqLzgrjWPBESEAJW30iD6SbQLs8MRf9tTEa79IimnNplkqvMieGgeCZ3EgZqnH
+D6KZ5S5aPkR8EV7AMOFEFLVCr3JwrhH0NGR6f/nqf53g2uviNNHsbNZkUJwsDn76
+/TzaRPLr4LcAG6i9+frvrVPEaS/75/QJrsSH93RGs9AaVzd2KPct/dDByQbpbMPb
+/eN27m9/8x+4X3Qb7JIPRxbvtjBR03lntBFrE/yxMdAYQt1GdOkPv+n754KDIlhC
+6+Z7HJuYkS+QJ1EkAnKG4Y6vnUes75HV7313DlPhTyvtdHyBy8yTTnzma33Gj2sp
+7IXtX/S0i79O
+-----END ENCRYPTED PRIVATE KEY-----
diff --git a/tests/files/rsa-pkcs12.pem b/tests/files/rsa-pkcs12.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-pkcs12.pem
@@ -0,0 +1,455 @@
+-----BEGIN PKCS12-----
+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
+IAAoAHIAcwBhACkAIAAtAG4AbwBtAGEAYw==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+Qbe5Scqyes+V0blmkYcECOYC+UPng6RsAgIIAA==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIGuAIBAzCCBl4GCSqGSIb3DQEHAaCCBk8EggZLMIIGRzCCAv8GCSqGSIb3DQEH
+BqCCAvAwggLsAgEAMIIC5QYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIctp8
+K6dnywACAggAgIICuNqYGCXbXrFZN+RUaOft2QS6RdIiI7upD9RcnnAZVx/Qjg54
+ar1kRTcFbWZTKLejxwgIJH3isXEIwN1vhxRPfRIXskMlTQQeDqQawyv15ehl98KF
+3xA50r9QjNnnlBvp4hDVuEHqcjVtM2BJ9HH/90RCmlh3C6v5qtxe9Sg3n/If3rK7
+Rn2jqxTTNd/E2z9mktODjjLU915D7RAaWeUbQvsgrqL8iIQEupuZ93NMV96K+b3P
+6uBA/HpMczfAYNv85ds4s4lKpVBk+6J6UvLoF4SYhSuCYHTgoIZQKKoJ0EneRFPW
+ytG0irSc4NtUDHCOPuFf7LxT0ettk5QIfyKTACzxrsqNKnK3AdYUtOZ0N5V8rZkl
+9+cl55oQHVyxsAtZ+03KavZGegsDUcifBJ/OC2lwvvzGkDIla2z8/OhMMD/+3lyw
+ARa95QZz3ffDUGS0DYtke3zc6axMCUFf38RH1WJuj9RAsD1F8Gxr/jaky6zVijCM
+hpGOXik64qy5rp94ntZ0PPnZ+WbuURWFKJn+8j/rLO85knHzADSgbIugJquk9gqx
+kkKWnMlTwhxD3LjvWppbn8RE8AopYseMWkTURy5B3YSVuxkvMfey8ZOzc5XV0gqB
+8rIMVW3YUoQftO/3pld9DlSsxI7dbnLVvi1hn4X5MOFdiSPEv67BiZd6KMegC91v
+4u6xTcJd5Urm6dBqg7OEgiFhc0JzIA4r+pwESRdgXd9CsIxXptGojXa4A8fnuqYb
+ey56VWd2PaTuLEI4rbIZSzAWbRsFQ/R6/rfoJX1cd0Wi6bYcddexX4VMIrs4rhOR
+LfkJQzO/hu//b9KEFCSMSPFr6WtvhvzleWe8vORuTXPxfnULGYpYP9Ndaa+9OIUm
+3XvNtoJyMEOBjJRMGkHm2eyH7FltchWKcnYqO6QJ0hbcGTQj6zCCA0AGCSqGSIb3
+DQEHAaCCAzEEggMtMIIDKTCCAyUGCyqGSIb3DQEMCgECoIICpjCCAqIwHAYKKoZI
+hvcNAQwBAzAOBAhU1RnM3d8KhwICCAAEggKAoqW80sLkBPyn5MZIGgwuloAzEts/
+zTqlOwPxhRzSQPbY6C+ZOq4AKGcNgM2LBTnFkwvBMnKja91I/oRMmUW1MjVpieHs
+FU8WvTAuxN8drahfoI2vmD+5TZc0d1lxfpAis0ibasV2WtrtSWlTyIcIYxITBT7K
+jtINMMLKIAdSdHXPHbZ3LYoZiehNQGHmgLc3plm73S6sbkJ147u7K8UO/0+Emwnm
+T693basVZZ0FQdU9RCTtgO6A0bobkiw5RDdDA1e7EzbZ1ANbsjvk9YPB0dzGDH8X
+ulaxLZb9Oon/6H9FMsG53GwV3RKQ4lTzK7Ur6sY0EkpFa4zHEIjoNuWDOujxffVL
+E2dYXBS9r3gCYfL9uLjjX1dDoLgCz+w+QK6LPFqG4ej9iQ2l+iI+qZ2Oc/qjAgHh
++Ym7Zjes+PwE2g9alwYVYCpx6QDjtprfoKC2KvS3dxiuu6ZwZwFpVQTiCTb0zO5K
+RspGZ8es1y2z0VugVU5QTRFgBRsYf5ULLRDF+OJnxO1Ac5q4H7e5zAscVmuL75yA
+F9N3F8ODxjfeZckzOnokWQTs3FPEa8R37HDOlOBGvRF32QArbWXgr1efmE2spoQh
+xe6esWG2w5VaNSP2fk8sz3cqBvZa19RxZlXDJBiKKp6x05QoIj7r5oJOHlERWp2s
+lOOlGCDTpemC/3PrYKb05UDMz1duMdbJmn3X7fwR5OrDJt5LnhW/TSjS0k5pOyFw
+DnHGphZhX6ChBDOEtgPX81eoL1iHwooqaVM5BXS4WdHXidp5gDbBEmPLGcgRmWeE
+Hf6+DNHwQhFUMS5f740p+cpDXeei8AsRNv6TndqW44WiInj5NFKZlXzHtzFsMCMG
+CSqGSIb3DQEJFTEWBBTAbCJKoqJORdb4i56jGIji040OsDBFBgkqhkiG9w0BCRQx
+OB42AFAASwBDAFMAMQAyACAAKAByAHMAYQApACAALQBtAGEAYwBhAGwAZwAgAHMA
+aABhADMAOAA0MFEwQTANBglghkgBZQMEAgIFAAQwIjeGs216au95fEcxTik5VnZV
+VSE7eqvIyhBRAshh1EtyB3sLY/MknrZqUrz3NqATBAiMSKkq/xXbXQICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIG5wIBAzCCBq0GCSqGSIb3DQEHAaCCBp4EggaaMIIGljCCA0IGCSqGSIb3DQEH
+BqCCAzMwggMvAgEAMIIDKAYJKoZIhvcNAQcBMFcGCSqGSIb3DQEFDTBKMCkGCSqG
+SIb3DQEFDDAcBAgRpECHAkPoOQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQME
+AQIEELLZPm7Bv6pFQBJjkdQJzLOAggLAWLiUOZ1f+LZyTZpBSOS5zMZO4vYSxkf8
+veYo8yAOZo2708+k8UErrPCZLLWXn90nTQPPGZ6cG1FFva5IgNH/kKh78i/tt/N2
+2OLAxs99IRO1KiE58jYYRElP/j///B/Hm6rNRPFmR1bTIn2NmUNlqtqA9tZbsh3l
+OHDy/Ct6pVDUwYVnle2uuvTesnOTF5LmI5vqox9KURwPJ3oKe5hz+FuLud/Xgo8a
+/T/83lVcwvIhbmhF3IXtNRjZkCKAoH9QtdTv0eNpeBqmS8BifHH9mghy+lJ4q1Ln
+vOyamuRlcuNMK0Sj2kFm0UaBhnMzYMpKevb5H9XjN6MmJyb1dNqVDT7ln3jxCOma
+a2NkKrdfLyMZk5puL9L9G0HAEvAoLznx7CfDzJo0O9aawhyeEqFT1FdpaUJMA72f
+3DaBRN8R/UmFvAFDgwPryyZO3oH4pKHk9Ukd4HL4t1XXiidyHtRW+dh7XqJpKS9d
+Z+IQxh4vzbYcTw6J+hE4Dc2lQIx4O6k7us9g0i7zqr7jPGPPz/Cq16l4BrwkjDns
+8mRL0p4l1MdwYol8DmTliebU4xmpj+VrbNOFjnYIDR2sVSPavx6Qv3G3TvMOBTfg
+kSJ84RbLgb3kuraVXXVMYq8/He2oVvIygdKIgd3+1a7kptcmoKzfqarJIVppKcJG
+HyH9FhrzlW2S2btlHRkzs4clOJspeogpn3hp6HiUGEGOw9Wgv5i0pbLXN9sv7r6J
+jIPkocwR1ktkkh6vGLEpsDZz8KFnaUDWxdZPUAlWqcWASwEMIiZxH+ptGt872p8s
+mqQ1Ha2vXXpFv6XtubBiBFDiqVvtmnvAwmQ4d+i1Wingc01+zTex/3ysD36B63dk
+WznOUqYA1ElPRCCoDPAX+5lyshKaIUpU4IA1IlWgKyVlI8bYlDMexj9W/0a3fKoe
+u81rYvBhtqYwggNMBgkqhkiG9w0BBwGgggM9BIIDOTCCAzUwggMxBgsqhkiG9w0B
+DAoBAqCCAqYwggKiMBwGCiqGSIb3DQEMAQMwDgQIFAaOoisB5rcCAggABIICgMDy
+9dPloTBLnJy9d+eGSNH+vb5D+t7RqtdjUD8Hmi2FYhi+MChire6qlhas3q0caBCU
+Sojl2kebiN6+DrJNTek3zD9yQg7FiqcUUukc98HIn4j1RiRrzkIzKo6WhYS2frr9
+pwlvlBScgzjPDCgt32P2mulZUfVlE8SIwSddXxD/uwBZgyzTIQX8+HqnPKjWVp1y
+b1rT7WOKt1ukG/fpVi3PHV4U1w1MbbuM3ZTD/ILoy6BaAOj7xUhRHxabjT+R08Qn
+EFbgNQnfC22fzVMqd/s/kYOBX3b1Lh+Wj9JaYXBPRZeXPexr7q1WUK07XmQMffX4
+jGYvSPiYvhaiDFv4Ykx9omzNgoWLfsNDfIH7koLAOkOSXxucWnG/3v2yhKBcPIwf
+C15yLrnwafIdEi/Lt1llhBcxbdunP3XF+DVRMCwpLYNAwgHEBLMd+sM7fdbaFk9h
+xJ7gWPk/aArzLPSayoX8UCFKdIFyyxvewRfhSEMZsXu21kwoOUWFPk5asrp/Fj6D
+iRxSMUJnEHrEqXi52Ldkz8xYU1Dfvf8Xx5N2rMWbnouPfa11VBcBSuVUxjaQp/JI
+ekoXBG7Nrw1TW51X2iqRaBRlrM4Lv7nts4Eth9pO+QghK1z8viOiJeIh3QQQUZoV
+0MD0ueqnun/faubwFwCL8qoqdphNaA6P/SYzULVB7R3eCqYK39rUCfS7GIo7e/0u
+j8EUVrpgSZapuSXUi4E0pHnnGnAXgmIRy70CrLZ9GvSg+tNRWhtYxrnxjNj6XyKS
+OkfH5lXfQTB1BOUvCzl5T7hHp5YiWHZ0Z4Ywf9K69F7zCZDKXhCf4XTq51wlKwJu
+clZxIBPw0ybRqXgk1hwxeDAjBgkqhkiG9w0BCRUxFgQUwGwiSqKiTkXW+IueoxiI
+4tONDrAwUQYJKoZIhvcNAQkUMUQeQgBQAEsAQwBTADEAMgAgACgAcgBzAGEAKQAg
+AC0AYwBlAHIAdABwAGIAZQAgAGEAZQBzAC0AMQAyADgALQBjAGIAYzAxMCEwCQYF
+Kw4DAhoFAAQUCwOOnafgzeuvFz6AlnAwAq69IM4ECL0D1SiU1yo+AgIIAA==
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+RUFHBwJNlAICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+bFxutU4CAggA
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIGYQIBAzCCBicGCSqGSIb3DQEHAaCCBhgEggYUMIIGEDCCAvcGCSqGSIb3DQEH
+BqCCAugwggLkAgEAMIIC3QYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIYcFE
+4SissJcCAggAgIICsDptp2pQvwMzaj3DMm37+kFDOAo8d11m48CUYwA+bBWV8PRH
+0TqUFcMxvdsI4NHliSRWMiWnWYuOhLFNGN64KIIj/uL2+6hCEw02cU5G3z4L23Kp
+VH7Bm1WqlY4tj0P6pLwwahv8rrYob/+lrx3qARX2AXkG7uMLpAiZolDtTIvkM3+L
+rZGEJYO/fqQMzfn0cyNYcTTa4FZrD508uRjb5MyfZNZjDW/hr/sZOX8SyLWl8Tgt
+OxuQDe579Y4YyP7uTgmSxnrqeVWs21I+F/qKjGhJimxapeZniMqZOv/rWZn4zx+x
+A+od3W1n4aFphxFxrOwKfgIVjBB+9bpifXsZZuzeTfsoG/BzkxlgzFxtoTphzMHH
++2qJt4NpfwfIGdtN2AKlYTun/hXniMoPoI6Q+oJLNuClp2M34rBGE5TkbO8u4rL1
+u1fPbZmAUvxIbyVtXgJz21hsY4CWtNDab0CUYuzL76sBwFu9gzVdsy431L8P/vzb
+wyCgVkzQd0MQLrWrIg/KDpj/jSm4ZvYj8WYgiNy7eGtoSKltjqGZEqHc1O1FQqU7
+RXUzoEalLdNdssWC0FItiZeCjEl/Ngtu8b5sg/Q4JlLsx773+5x42C1Q55mJu9pi
+WDM/v5bMDzDTqi9ZZet7b8nj2qxtICqClsMOqpmsguuGnXtnBusjOvhnjV28ha1B
+dkma3q0PKYpjFmPBTEmH2SHUon0z4c7YEoJzAOXILnMYxEzUSqVmt2HZbErFmQ8E
+UQzf3M/fBie5sfqv1RbD6FUnkbmli5Xq1N5IM3Ww0v4r1QkzF+Q6UV4gvcCrpYk0
+R3Of7QbWYzGvdTPDYUgn+9XOJ0RB4QeIVgPvLrCf38V7JvndanS2Jt8nJrglsYgm
+AYigb6Wml+KmSAFvkp2xAf62wLwPqX1ZTi9zoaMwggMRBgkqhkiG9w0BBwGgggMC
+BIIC/jCCAvowggL2BgsqhkiG9w0BDAoBAaCCAnswggJ3AgEAMA0GCSqGSIb3DQEB
+AQUABIICYTCCAl0CAQACgYEAtBG2kKWfYT1jUXmbDlrkD3Wfw0rKDjTdjjSswM2H
+ggMtEbiutc+cxnoW+DaeP1SzSdY7NXlaqvzftCPLvzJNaRM4HpG/XLqIdub5hzis
+brwN1/Q77zmUvr1ka7NaXKZDuuepORJsNh5lHkp22VR0O0pQ3zLNjX8IVAGkdpoE
+5iMCAwEAAQKBgEO5BbiREcg4lknmOnLDrFJEIroIPsXpDAqXtQEuS3CSUTkBBHRM
+iOH8uPbRU+LtsCBs+ge6hGcag+f0LoTSHlpsxqAlSeFPA7qwpUHPbs1iw2mw9Wxp
+eB2iYgc0NQVx+L8jX1ASHi25rcQ5udAzjTTtOPZq5m61ScQnipCyEl2BAkEA34Xm
+lXR282mQSvEcEzw7zEz8S7kjJxaXvnrGySoliuxdMLIuAune80sUIicubXgy9GZZ
+tW4SthfNqaoSVXtc+QJBAM47g+OYBQKk0vRw6431ghreZEu1bRyqctS03Zm2ozec
+UbT8qFW9AOwqmKl0CnuoxYGPascbPwEvvcBE6708LvsCQBNqsU4YUODyMZug+Dxf
+hh5ILb5yNbCGkOX2CmCdLae0wp+hSsfsAvcFdZlF6A2QXHTIk1BkYHG6/Z2YbYFJ
+dxkCQQCbSR73CWmEYx1g54HGY30yxA/bHeHpusI6PXG6o1XksrSnRbNu06DVMwG+
+XlziXeNRue6Zu39GYm9LTdn/pEhvAkEAuBNeBwlDNqJoPav9crsIJgctWmO80hMd
+kWf7Nyvw10PWPiFZYgggr8PdQkjKY34a7IbeHU+m8A8KoBwOX5Y5OTFoMCMGCSqG
+SIb3DQEJFTEWBBTAbCJKoqJORdb4i56jGIji040OsDBBBgkqhkiG9w0BCRQxNB4y
+AFAASwBDAFMAMQAyACAAKAByAHMAYQApACAALQBrAGUAeQBwAGIAZQAgAE4ATwBO
+AEUwMTAhMAkGBSsOAwIaBQAEFDcgpO09ef9GqZ+V8Imrbcos0A+cBAiDiF78z7Z0
+AgICCAA=
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+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
+kZqPBPMCAggA
+-----END PKCS12-----
+-----BEGIN PKCS12-----
+MIIGugIBAzCCBoAGCSqGSIb3DQEHAaCCBnEEggZtMIIGaTCCAw8GCSqGSIb3DQEH
+BqCCAwAwggL8AgEAMIIC9QYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYwDgQIi29c
+ZJLr798CAggAgIICyDX4RQtAkr7o637GGgni5Y2Ah12PRo4E5WKIlZYW9aR7qulg
+upU3/k7QR9Y85XR+7lmbNW1p8U7waU37BQiJCdMdXyU3Q17szHh5SR4PeGZy+TUY
+Wx1FLzbPBVghTDLU1h7lIGGLwi/IjuxLXglUtSQ/Q1/dYsWBbVbXdIZy5MWKTNx9
+PztHjqe2078nmaZ4u3cfzh0aPqexMy1E955EtaUrV/MLXteo1C1MvrZ7HtZ/4JZy
+3bnoO8OQMpruOjDzR3Yis/PQRH5khvMDbzrQEE7z3P/+snev8SlEIHLGZpn/h2RN
+FCV3+y0slGkph4cLt3aAda4iyQGF6G+K5y+YpJ7IJkF8UPcJmvtQB7w7e6ZeET7M
+3cf6jQJdU9G0/VLPvSoVf7ye508OiO35lEQxLLluyWW1vfkhdZ/EHPJdxZ2rOQaj
+jiuqdQomdXSnp3pJkMcXpsbruwG+N80Ja04vaXEf13OGnQmF54+NVPdTbLYKsBNQ
+WRSylgnKY/aIfR6E6EfhhzYvn/6FhhHTIsc0Vn4BRpOeN7eKfgaZeQuejV/bY9+p
+Ckd/ZoOnnDPtQGrP9gH69mspdMJ68cFDu9DqJiTgP+aOLb6s9NZh5dT6n0iwz5rH
+fCA3H3zS8YdD5qE15Fg82oiR5yUsyvyxPEIFhTRbpyv+HYMt1hUzBPCQX/xv5hNW
++aibkfayFVizuXjpDkrcAiNpCNYIutatIXIjFZptPztrk114l9MqFVt7Wt72M87E
+D0kAzz9Yr+w2q8qEEcE7Uh/auevsTGBhvuwSu0oE3jtvHpGaEg/hOTwbylrEobvy
+jeVYag3HQAnDy8X1XcrJFmm7kVou8Z9dFnaP+lZOfMjESUDPiX1XrXWDIMWZvdh4
+jpikJG7Roaaegkq288HwyQGWCcxFG+7m9XWs/RQhc3nPgLemp70DeQu+HTiQKoPV
+sO5qSS8wggNSBgkqhkiG9w0BBwGgggNDBIIDPzCCAzswggM3BgsqhkiG9w0BDAoB
+AqCCAqYwggKiMBwGCiqGSIb3DQEMAQYwDgQIA936U0ZbYmMCAggABIICgPFqmMDS
+MDZjXJg1scRG+eBc7fZ8zt64P3k46BSev9XPG2y+dKuj31Yuk9K/2CPsYO1b4fvo
+i0SWb5I85K5kbmVMvEFTS8bRO8RTDzAXf9HVz9S0inX3uqJCQ/lUp1IMadLYyghc
+E63Ag+S7Ob74puCSCoHMwKIilHn3+iKZRK+OO8IYiFvOsoj4kdOI2Vd0AtWppIoq
+G2GcnUu9cxbyab1VTuEdMoV6cpTiB9yfZUytxrdAR4368eN+xYRTvyY95wyxW0mN
+jUuOhN9UZBNH76GNhCj/yeI4J08K7QBxehsTAtpHfo35i3c67sCC3EFjXckRNGL9
+nfN3G1i3kGfuilaV3qACxHa6i0Z4DFhF+MG42kUMdQOC5I/tPqGyXI4JUGfU7CwE
+FAt2tAxSvUw4H2sJGnGYAOjuWscDkbTAm0qKxXYpMOXIliTjW2DwNxlGBnLuqNOE
+sV/OxQwKVZx2RCPmJcfddLaUfPRbHmJu3VGXi/CQnoIaCIL+m/veLWbJrAsngc7V
+8a0Bn1oHsZXjzNuAOZxT/rNX72AOCJdtIaXzfCPvzC1sM2scoDVsqKSgxIyqUXMk
+4XEYP6bgxqaDlFSBImENIvWcX1ekryw9JKgOP+uIaW8DLmumU1J6S4exZFxTRKh5
+2hDI8/Fjry4QnDSDxNyt1Mie77uKEzPt8fHmwRGbs1YCA6y7X+qD5JBkZSrn+dAs
+YIgH7UZvbBxaN1NrQDwJjLsLhfj4g8L7dqMXYINXh3RhTeBm6sosD8EOaHcVReqm
+7dw2Ej7T982PyCSVD/BQfBaB7KPQy7GVCYWRDcK0R7QfiIHkKMyfgJDXvL8DnxtX
+6Xtt+6Vrz/uKQsYxfjAjBgkqhkiG9w0BCRUxFgQUwGwiSqKiTkXW+IueoxiI4tON
+DrAwVwYJKoZIhvcNAQkUMUoeSABQAEsAQwBTADEAMgAgACgAcgBzAGEAKQAgAC0A
+awBlAHkAcABiAGUAIABQAEIARQAtAFMASABBADEALQBSAEMAMgAtADQAMDAxMCEw
+CQYFKw4DAhoFAAQUoSm7SB3q7Zpmgsmf7YtgJpq6RakECP3zlmAjUXYJAgIIAA==
+-----END PKCS12-----
diff --git a/tests/files/rsa-public.pem b/tests/files/rsa-public.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-public.pem
@@ -0,0 +1,11 @@
+-----BEGIN PUBLIC KEY-----
+MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0EbaQpZ9hPWNReZsOWuQPdZ/D
+SsoONN2ONKzAzYeCAy0RuK61z5zGehb4Np4/VLNJ1js1eVqq/N+0I8u/Mk1pEzge
+kb9cuoh25vmHOKxuvA3X9DvvOZS+vWRrs1pcpkO656k5Emw2HmUeSnbZVHQ7SlDf
+Ms2NfwhUAaR2mgTmIwIDAQAB
+-----END PUBLIC KEY-----
+-----BEGIN RSA PUBLIC KEY-----
+MIGJAoGBALQRtpCln2E9Y1F5mw5a5A91n8NKyg403Y40rMDNh4IDLRG4rrXPnMZ6
+Fvg2nj9Us0nWOzV5Wqr837Qjy78yTWkTOB6Rv1y6iHbm+Yc4rG68Ddf0O+85lL69
+ZGuzWlymQ7rnqTkSbDYeZR5KdtlUdDtKUN8yzY1/CFQBpHaaBOYjAgMBAAE=
+-----END RSA PUBLIC KEY-----
diff --git a/tests/files/rsa-self-signed-cert.pem b/tests/files/rsa-self-signed-cert.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-self-signed-cert.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICEDCCAXmgAwIBAgIJAPcN61ksKTebMA0GCSqGSIb3DQEBCwUAMCExHzAdBgkq
+hkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMTgwNzAxMDkzNTE0WhcNMTgw
+NzMxMDkzNTE0WjAhMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMIGf
+MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0EbaQpZ9hPWNReZsOWuQPdZ/DSsoO
+NN2ONKzAzYeCAy0RuK61z5zGehb4Np4/VLNJ1js1eVqq/N+0I8u/Mk1pEzgekb9c
+uoh25vmHOKxuvA3X9DvvOZS+vWRrs1pcpkO656k5Emw2HmUeSnbZVHQ7SlDfMs2N
+fwhUAaR2mgTmIwIDAQABo1AwTjAdBgNVHQ4EFgQU/JTTK/+8Q86nWQwPMu+1PpT6
+Bg4wHwYDVR0jBBgwFoAU/JTTK/+8Q86nWQwPMu+1PpT6Bg4wDAYDVR0TBAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOBgQCfbJlP9Y26k7fg/FV1ZTrlf2qYITgC1kottFrS
+JKSQRj3rWioOIxVaJo3Sw9V5XUag0LP0nyUspoSRHxvQXgqUnQrjhX7k/Qe9vVSc
+pXZKw3GSY1vHCM7xD71o2N3girECdLJDjnIw3grnHUI4kVdARMkCkVaEtnGADSGq
+ei1AZw==
+-----END CERTIFICATE-----
diff --git a/tests/files/rsa-unencrypted-pkcs8.pem b/tests/files/rsa-unencrypted-pkcs8.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-unencrypted-pkcs8.pem
@@ -0,0 +1,16 @@
+-----BEGIN PRIVATE KEY-----
+MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALQRtpCln2E9Y1F5
+mw5a5A91n8NKyg403Y40rMDNh4IDLRG4rrXPnMZ6Fvg2nj9Us0nWOzV5Wqr837Qj
+y78yTWkTOB6Rv1y6iHbm+Yc4rG68Ddf0O+85lL69ZGuzWlymQ7rnqTkSbDYeZR5K
+dtlUdDtKUN8yzY1/CFQBpHaaBOYjAgMBAAECgYBDuQW4kRHIOJZJ5jpyw6xSRCK6
+CD7F6QwKl7UBLktwklE5AQR0TIjh/Lj20VPi7bAgbPoHuoRnGoPn9C6E0h5abMag
+JUnhTwO6sKVBz27NYsNpsPVsaXgdomIHNDUFcfi/I19QEh4tua3EObnQM4007Tj2
+auZutUnEJ4qQshJdgQJBAN+F5pV0dvNpkErxHBM8O8xM/Eu5IycWl756xskqJYrs
+XTCyLgLp3vNLFCInLm14MvRmWbVuErYXzamqElV7XPkCQQDOO4PjmAUCpNL0cOuN
+9YIa3mRLtW0cqnLUtN2ZtqM3nFG0/KhVvQDsKpipdAp7qMWBj2rHGz8BL73AROu9
+PC77AkATarFOGFDg8jGboPg8X4YeSC2+cjWwhpDl9gpgnS2ntMKfoUrH7AL3BXWZ
+RegNkFx0yJNQZGBxuv2dmG2BSXcZAkEAm0ke9wlphGMdYOeBxmN9MsQP2x3h6brC
+Oj1xuqNV5LK0p0WzbtOg1TMBvl5c4l3jUbnumbt/RmJvS03Z/6RIbwJBALgTXgcJ
+QzaiaD2r/XK7CCYHLVpjvNITHZFn+zcr8NdD1j4hWWIIIK/D3UJIymN+GuyG3h1P
+pvAPCqAcDl+WOTk=
+-----END PRIVATE KEY-----
diff --git a/tests/files/rsa-unencrypted-trad.pem b/tests/files/rsa-unencrypted-trad.pem
new file mode 100644
--- /dev/null
+++ b/tests/files/rsa-unencrypted-trad.pem
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXQIBAAKBgQC0EbaQpZ9hPWNReZsOWuQPdZ/DSsoONN2ONKzAzYeCAy0RuK61
+z5zGehb4Np4/VLNJ1js1eVqq/N+0I8u/Mk1pEzgekb9cuoh25vmHOKxuvA3X9Dvv
+OZS+vWRrs1pcpkO656k5Emw2HmUeSnbZVHQ7SlDfMs2NfwhUAaR2mgTmIwIDAQAB
+AoGAQ7kFuJERyDiWSeY6csOsUkQiugg+xekMCpe1AS5LcJJROQEEdEyI4fy49tFT
+4u2wIGz6B7qEZxqD5/QuhNIeWmzGoCVJ4U8DurClQc9uzWLDabD1bGl4HaJiBzQ1
+BXH4vyNfUBIeLbmtxDm50DONNO049mrmbrVJxCeKkLISXYECQQDfheaVdHbzaZBK
+8RwTPDvMTPxLuSMnFpe+esbJKiWK7F0wsi4C6d7zSxQiJy5teDL0Zlm1bhK2F82p
+qhJVe1z5AkEAzjuD45gFAqTS9HDrjfWCGt5kS7VtHKpy1LTdmbajN5xRtPyoVb0A
+7CqYqXQKe6jFgY9qxxs/AS+9wETrvTwu+wJAE2qxThhQ4PIxm6D4PF+GHkgtvnI1
+sIaQ5fYKYJ0tp7TCn6FKx+wC9wV1mUXoDZBcdMiTUGRgcbr9nZhtgUl3GQJBAJtJ
+HvcJaYRjHWDngcZjfTLED9sd4em6wjo9cbqjVeSytKdFs27ToNUzAb5eXOJd41G5
+7pm7f0Zib0tN2f+kSG8CQQC4E14HCUM2omg9q/1yuwgmBy1aY7zSEx2RZ/s3K/DX
+Q9Y+IVliCCCvw91CSMpjfhrsht4dT6bwDwqgHA5fljk5
+-----END RSA PRIVATE KEY-----
