packages feed

x509-ocsp (empty) → 0.1.0.0

raw patch · 13 files changed

+673/−0 lines, 13 filesdep +HUnitdep +asn1-encodingdep +asn1-typessetup-changedbinary-added

Dependencies added: HUnit, asn1-encoding, asn1-types, base, bytestring, cryptohash-sha1, crypton-x509, pem, x509-ocsp

Files

+ Changelog.md view
@@ -0,0 +1,4 @@+### 0.1.0.0++- Initial version.+
+ Data/X509/AIA.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE RecordWildCards, PatternSynonyms #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Data.X509.AIA+-- Copyright   :  (c) Alexey Radkov 2024+-- License     :  BSD-style+--+-- Maintainer  :  alexey.radkov@gmail.com+-- Stability   :  experimental+-- Portability :  portable+--+-- Encode and decode X509 Authority Information Access extension.+--+-- This module complies with /rfc5280/.+-----------------------------------------------------------------------------++module Data.X509.AIA (ExtAuthorityInfoAccess (..)+                     ,AuthorityInfoAccess (..)+                     ,AIAMethod (..)+                     ) where++import Data.X509+import Data.ASN1.Types+import Data.ByteString (ByteString)++pattern OidAIA :: [Integer]+pattern OidAIA = [1, 3, 6, 1, 5, 5, 7, 1, 1]++pattern OidOCSP :: [Integer]+pattern OidOCSP = [1, 3, 6, 1, 5, 5, 7, 48, 1]++pattern OidCAIssuers :: [Integer]+pattern OidCAIssuers = [1, 3, 6, 1, 5, 5, 7, 48, 2]++-- | Authority Info Access extension.+newtype ExtAuthorityInfoAccess = ExtAuthorityInfoAccess [AuthorityInfoAccess]+    deriving (Show, Eq)++-- | Authority Info Access description.+data AuthorityInfoAccess = AuthorityInfoAccess { aiaMethod :: AIAMethod+                                               , aiaLocation :: ByteString+                                               } deriving (Show, Eq)++-- | Method of Authority Info Access (/OCSP/ or /CA issuers/).+data AIAMethod = OCSP | CAIssuers deriving (Show, Eq)++instance OIDable AIAMethod where+    getObjectID OCSP = OidOCSP+    getObjectID CAIssuers = OidCAIssuers++instance OIDNameable AIAMethod where+    fromObjectID OidOCSP = Just OCSP+    fromObjectID OidCAIssuers = Just CAIssuers+    fromObjectID _ = Nothing++data DecState = DecStart | DecMethod | DecLocation | DecEnd++instance Extension ExtAuthorityInfoAccess where+    extOID = const OidAIA+    extHasNestedASN1 = const True+    extEncode (ExtAuthorityInfoAccess aia) =+        Start Sequence+        : concatMap (\AuthorityInfoAccess {..} ->+                        [ Start Sequence+                        , OID $ getObjectID aiaMethod+                        , Other Context 6 aiaLocation+                        , End Sequence+                        ]+                    ) aia+        ++ [End Sequence]+    extDecode [Start Sequence, End Sequence] =+        Right $ ExtAuthorityInfoAccess []+    extDecode (Start Sequence : encAia) =+        go DecStart Nothing encAia []+        where go DecStart Nothing (Start Sequence : next) =+                  go DecMethod Nothing next+              go DecMethod Nothing (OID v : next) =+                  go DecLocation (Just v) next+              go DecLocation (Just v) (Other Context 6 s : next) =+                  case fromObjectID v of+                      Nothing -> const $ Left "bad AIA method"+                      Just v' -> go DecEnd Nothing next+                                        . (AuthorityInfoAccess v' s :)+              go DecEnd Nothing (End Sequence : next@(Start Sequence : _)) =+                  go DecStart Nothing next+              go DecEnd Nothing [End Sequence, End Sequence] =+                  Right . ExtAuthorityInfoAccess . reverse+              go _ _ _ = const $ Left "bad or incompatible AIA sequence"+    extDecode _ =+        Left "bad AIA sequence"+
+ Data/X509/OCSP.hs view
@@ -0,0 +1,241 @@+{-# LANGUAGE LambdaCase, ViewPatterns, PatternSynonyms #-}++-----------------------------------------------------------------------------+-- |+-- Module      :  Data.X509.OCSP+-- Copyright   :  (c) Alexey Radkov 2024+-- License     :  BSD-style+--+-- Maintainer  :  alexey.radkov@gmail.com+-- Stability   :  experimental+-- Portability :  portable+--+-- Encode and decode X509 OCSP requests and responses.+--+-- This module complies with /rfc6960/.+-----------------------------------------------------------------------------++module Data.X509.OCSP (CertId (..)+                      ,encodeOCSPRequestASN1+                      ,encodeOCSPRequest+                      ,OCSPResponse (..)+                      ,OCSPResponseStatus (..)+                      ,OCSPResponsePayload (..)+                      ,OCSPResponseCertData (..)+                      ,OCSPResponseCertStatus (..)+                      ,decodeOCSPResponse+                      ) where++import Data.X509+import Data.ASN1.Types+import Data.ASN1.Encoding+import Data.ASN1.BinaryEncoding+import Data.ASN1.Stream+import Data.ASN1.Error+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as L+import Data.Int+import Data.Word+import Data.Bits+import Crypto.Hash.SHA1+import Control.Arrow++pattern OidAlgorithmSHA1 :: [Integer]+pattern OidAlgorithmSHA1 = [1, 3, 14, 3, 2, 26]++pattern OidBasicOCSPResponse :: [Integer]+pattern OidBasicOCSPResponse = [1, 3, 6, 1, 5, 5, 7, 48, 1, 1]++derLWidth :: Word8 -> Int64+derLWidth x | testBit x 7 = succ $ fromIntegral $ x .&. 0x7f+            | otherwise = 1++issuerDNHash :: Certificate -> ByteString+issuerDNHash cert = hashlazy $ encodeASN1 DER dn+    where dn = toASN1 (certIssuerDN cert) []++pubKeyHash :: Certificate -> ByteString+pubKeyHash cert = hashlazy $ L.drop (succ $ derLWidth $ L.head pk) pk+    where pk = case toASN1 (certPubKey cert) [] of+                   Start Sequence+                     : Start Sequence+                     : OID _+                     : _+                     : End Sequence+                     : v@(BitString _)+                     : _ -> L.drop 1 $ encodeASN1 DER $ pure v+                   _ -> error "bad pubkey sequence"++-- | Certificate Id.+--+-- This data is used when building OCSP requests and parsing OCSP responses.+data CertId = CertId { certIdIssuerNameHash :: ByteString+                       -- ^ Value of /issuerNameHash/ as defined in /rfc6960/+                     , certIdIssuerKeyHash :: ByteString+                       -- ^ Value of /issuerKeyHash/ as defined in /rfc6960/+                     , certIdSerialNumber :: Integer+                       -- ^ Serial number of checked certificate+                     } deriving (Show, Eq)++-- | Build and encode OCSP request in ASN1 format.+--+-- The returned value contains the encoded request and an object of type+-- 'CertId' with hashes calculated by /SHA1/ algorithm.+encodeOCSPRequestASN1+    :: Certificate              -- ^ Issuer certificate+    -> Certificate              -- ^ Checked certificate+    -> ([ASN1], CertId)+encodeOCSPRequestASN1 issuerCert cert =+    let h1 = issuerDNHash cert+        h2 = pubKeyHash issuerCert+        sn = certSerial cert+    in ( [ Start Sequence+         , Start Sequence+         , Start Sequence+         , Start Sequence+         , Start Sequence+         , Start Sequence+         , OID OidAlgorithmSHA1+         , Null+         , End Sequence+         , OctetString h1+         , OctetString h2+         , IntVal sn+         , End Sequence+         , End Sequence+         , End Sequence+         , End Sequence+         , End Sequence+         ]+       , CertId h1 h2 sn+       )++-- | Build and encode OCSP request in ASN1 DER format.+--+-- The returned value contains the encoded request and an object of type+-- 'CertId' with hashes calculated by /SHA1/ algorithm.+encodeOCSPRequest+    :: Certificate              -- ^ Issuer certificate+    -> Certificate              -- ^ Checked certificate+    -> (L.ByteString, CertId)+encodeOCSPRequest = (first (encodeASN1 DER) .) . encodeOCSPRequestASN1++-- | OCSP response data.+data OCSPResponse =+    OCSPResponse { ocspRespStatus :: OCSPResponseStatus+                   -- ^ Response status+                 , ocspRespPayload :: Maybe OCSPResponsePayload+                   -- ^ Response payload data+                 } deriving (Show, Eq)++-- | Status of OCSP response as defined in /rfc6960/.+data OCSPResponseStatus = OCSPRespSuccessful+                        | OCSPRespMalformedRequest+                        | OCSPRespInternalError+                        | OCSPRespUnused1+                        | OCSPRespTryLater+                        | OCSPRespSigRequired+                        | OCSPRespUnauthorized+                        deriving (Show, Eq, Bounded, Enum)++-- | OCSP response payload data.+data OCSPResponsePayload =+    OCSPResponsePayload { ocspRespCertData :: OCSPResponseCertData+                          -- ^ Checked certificate data+                        , ocspRespData :: [ASN1]+                          -- ^ Whole response payload+                        } deriving (Show, Eq)++-- | OCSP response certificate data.+data OCSPResponseCertData =+    OCSPResponseCertData { ocspRespCertStatus :: OCSPResponseCertStatus+                           -- ^ Status of checked certificate+                         , ocspRespCertThisUpdate :: ASN1+                           -- ^ Value of /thisUpdate/ as defined in /rfc6960/+                         , ocspRespCertNextUpdate :: Maybe ASN1+                           -- ^ Value of /nextUpdate/ as defined in /rfc6960/+                         } deriving (Show, Eq)++-- | Status of the checked certificate as defined in /rfc6960/.+data OCSPResponseCertStatus = OCSPRespCertGood+                            | OCSPRespCertRevoked+                            | OCSPRespCertUnknown+                            deriving (Show, Eq, Bounded, Enum)++-- | Decode OCSP response.+--+-- Value of the /certificate id/ is expected to be equal to what was returned+-- by 'encodeOCSPRequest': it is used to check the correctness of the response.+--+-- /Left/ value gets returned on parse errors detected by 'decodeASN1'.+-- /Right/ value with /Nothing/ gets returned on unexpected ASN1 contents.+decodeOCSPResponse+    :: CertId                   -- ^ Certificate Id+    -> L.ByteString             -- ^ OCSP response+    -> Either ASN1Error (Maybe OCSPResponse)+decodeOCSPResponse certId resp = decodeASN1 DER resp >>= \case+    [ Start Sequence+      , Enumerated (toEnum . fromIntegral -> v)+      , End Sequence+      ] -> return $ Just $ OCSPResponse v Nothing+    [ Start Sequence+      , Enumerated (toEnum . fromIntegral -> v)+      , Start (Container Context 0)+      , Start Sequence+      , OID OidBasicOCSPResponse+      , OctetString resp'+      , End Sequence+      , End (Container Context 0)+      , End Sequence+      ] -> do+          pl <- decodeASN1 DER $ L.fromStrict resp'+          return $+              case pl of+                  Start Sequence+                    : Start Sequence+                    : Start (Container Context ctx)+                    : c1 | ctx `elem` [0..2] ->+                        let skipVersion =+                                if ctx == 0+                                    then drop 1 . skipCurrentContainer+                                    else id+                        in Just $ getCurrentContainerContents $+                               drop 2 $ skipCurrentContainer $ skipVersion c1+                  _ -> Nothing+              >>= \case+                      Start Sequence+                        : Start Sequence+                        : Start Sequence+                        : OID _+                        : _+                        : End Sequence+                        : OctetString h1+                        : OctetString h2+                        : IntVal sn+                        : End Sequence+                        : c2 | CertId h1 h2 sn == certId ->+                            case c2 of+                                Other Context (toEnum -> n) _+                                  : c3 -> Just (n, c3)+                                Start (Container Context (toEnum -> n))+                                  : c3 -> Just (n, skipCurrentContainer c3)+                                _ -> Nothing+                      _ -> Nothing+              >>= \(n, tc1) -> case tc1 of+                                   tu@(ASN1Time TimeGeneralized _ _)+                                     : c4 -> Just (n, tu, c4)+                                   _ -> Nothing+              >>= \(st, tu, tc2) ->+                  let nu = case tc2 of+                               Start (Container Context 0)+                                 : t@(ASN1Time TimeGeneralized _ _)+                                 : End (Container Context 0)+                                 : _ -> Just t+                               _ -> Nothing+                  in Just $ OCSPResponse v $+                         Just $ OCSPResponsePayload+                             (OCSPResponseCertData st tu nu) pl+    _ -> return Nothing+    where getCurrentContainerContents = fst . getConstructedEnd 0+          skipCurrentContainer = snd . getConstructedEnd 0+
+ LICENSE view
@@ -0,0 +1,25 @@+The following license covers this documentation, and the source code, except+where otherwise indicated.++Copyright 2024, Alexey Radkov. 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.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 HOLDERS 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.
+ README.md view
@@ -0,0 +1,89 @@+Basic X509 OCSP implementation in Haskell+=========================================++[![Build Status](https://github.com/lyokha/x509-ocsp/workflows/CI/badge.svg)](https://github.com/lyokha/x509-ocsp/actions?query=workflow%3ACI)+[![Hackage](https://img.shields.io/hackage/v/x509-ocsp.svg?label=hackage%20%7C%20x509-ocsp&logo=haskell&logoColor=%239580D1)](https://hackage.haskell.org/package/x509-ocsp)++This module helps building OCSP requests and parse OCSP responses in Haskell.++There are many scenarios of OCSP use. One of the simplest practical use cases+for clients is sending OCSP requests to the OCSP responder upon validation of+the server certificate during the TLS handshake. See the basic implementation+of this scenario in directory [*client-ocsp*](test/client-ocsp). To test this+scenario, run++```ShellSession+$ cd test/client-ocsp+$ cabal build+$ nginx -c /path/to/x509-ocsp/test/client-ocsp/nginx.conf+```++You may need to make the root certificate trusted by the system before running+Nginx. Below is how to do this in *Fedora*.++```ShellSession+$ sudo trust anchor --store ../data/certs/root/rootCA.crt+$ sudo update-ca-trust+```++Now let's create a database file *index.txt* (its path must be equal to what+written in clause *database* in file *../data/certs/openssl.cnf*),++```ShellSession+$ touch ../data/index.txt+```++and put there the server certificate.++```ShellSession+$ openssl ca -valid ../data/certs/server/server.crt -keyfile ../data/certs/root/rootCA.key -cert ../data/certs/root/rootCA.crt -config ../data/certs/openssl.cnf+```++The certificate has *good* status because we used option *-valid*. Run OpenSSL+OCSP responder in a separate terminal window to see what it will print out.++```ShellSession+$ openssl ocsp -index ../data/index.txt -port 8081 -rsigner ../data/certs/root/rootCA.crt -rkey ../data/certs/root/rootCA.key -CA ../data/certs/root/rootCA.crt -text+```++Run the test. It should print *Response: In backend 8010*.++```ShellSession+$ cabal run+Response: In backend 8010+```++The output of the OCSP responder must contain details of the request and the+response.++Let's revoke the certificate.++```ShellSession+$ openssl ca -revoke ../data/certs/server/server.crt -keyfile ../data/certs/root/rootCA.key -cert ../data/certs/root/rootCA.crt -config ../data/certs/openssl.cnf+```++Restart the OCSP responder and look at what *cabal run* will print (you may also+run *cabal run* twice instead of restarting the responder).++```ShellSession+$ cabal run+client-ocsp: HttpExceptionRequest Request {+  host                 = "localhost"+  port                 = 8010+  secure               = True+  requestHeaders       = []+  path                 = "/"+  queryString          = ""+  method               = "GET"+  proxy                = Nothing+  rawBody              = False+  redirectCount        = 10+  responseTimeout      = ResponseTimeoutDefault+  requestVersion       = HTTP/1.1+  proxySecureMode      = ProxySecureWithConnect+}+ (InternalException (HandshakeFailed (Error_Protocol "certificate rejected: [CacheSaysNo \"OCSP: bad certificate status OCSPRespCertRevoked\"]" CertificateUnknown)))+```++ The certificate has been revoked and the handshake fails.+
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple+main = defaultMain+
+ test/data/certs/root/rootCA.crt view
@@ -0,0 +1,24 @@+-----BEGIN CERTIFICATE-----+MIIEETCCAvmgAwIBAgIUQEy4pp/hZGsF+9Yi5Nkz7A6IOxswDQYJKoZIhvcNAQEL+BQAwgZcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH+DA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQKDApNeSBDb21wYW55MRIwEAYDVQQLDAlP+Q1NQIFRlc3QxGDAWBgNVBAMMD015IENvbXBhbnkgUm9vdDEYMBYGCgmSJomT8ixk+AQEMCHRlc3RPQ1NQMB4XDTI0MDIwNTExMDU0OVoXDTI1MDIwNDExMDU0OVowgZcx+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4g+RnJhbmNpc2NvMRMwEQYDVQQKDApNeSBDb21wYW55MRIwEAYDVQQLDAlPQ1NQIFRl+c3QxGDAWBgNVBAMMD015IENvbXBhbnkgUm9vdDEYMBYGCgmSJomT8ixkAQEMCHRl+c3RPQ1NQMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5xat2GzvwJsD+xI4wjKocsTjemx6wLR5bc9AWmOOiMcj/5b2OQp/0ng82GiluAsMECxBh1Rs2SPTY+GvCWdBQRF4ohKhKafmZAbjy/wIHEn/xC9dN6HxNRaOqB2Mt9WQKz8j/e2JxlQb9Q+JBpDWNMBToqleEv3TCajNat8Fal2DA5nXgenFhSj9WhJHHVfFeXwH3OAMsVLPmCb+fq3H9sSqCAJU4TRsgB/Wwt/OGSm2p74A5QlRuNkgviedmEuS4RfqtLS/gzQTObNg+kjT0xSN374sloKAYlTiQAYKuWtjpnJjkxmQJcDGEAzLDesYwmy0bC85MGov3ebfj+hYc2GTClawIDAQABo1MwUTAdBgNVHQ4EFgQUloJQHAxQscJA5gjKYv4f/gPGkxow+HwYDVR0jBBgwFoAUloJQHAxQscJA5gjKYv4f/gPGkxowDwYDVR0TAQH/BAUwAwEB+/zANBgkqhkiG9w0BAQsFAAOCAQEAATDeVc18pvx0OLv+hItQ21izepga63zOi/B2+9Cb+UrZvmJWajlWXqy6azntl9MXei3SSH6ojjlpbaZkKk4oVXVyyj2R9UT3GYlYM+olTD43+DSWMXWfbwOqnxgORYCnp8LF4Wx8QbcMhyuqceuvaV6/GyyjCjWCbU7m8a+13Je1+PnuG9d/iqhIilvVVNnyF2zo0jrsCeVL26xiQo8AEWLw9MN2m7hlAGLS4Ht+2wlJWP1Lr3+Ib5S4WGE3jYL1onQUhiP/eklCnAE9ynb44sxSwq9OsQ0KCNGO11ZP+i+pHOm0GnHYDZNvlsj4ODn/RfvnnzafYHldr2QKACqQRM/fzBA==+-----END CERTIFICATE-----
+ test/data/certs/server-chain.crt view
@@ -0,0 +1,49 @@+-----BEGIN CERTIFICATE-----+MIIESjCCAzKgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMCVVMx+EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEzAR+BgNVBAoMCk15IENvbXBhbnkxEjAQBgNVBAsMCU9DU1AgVGVzdDEYMBYGA1UEAwwP+TXkgQ29tcGFueSBSb290MRgwFgYKCZImiZPyLGQBAQwIdGVzdE9DU1AwHhcNMjQw+MjA1MTEwNzA2WhcNMjUwMjA0MTEwNzA2WjCBkTELMAkGA1UEBhMCVVMxEzARBgNV+BAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEzARBgNVBAoM+Ck15IENvbXBhbnkxEjAQBgNVBAsMCU9DU1AgVGVzdDESMBAGA1UEAwwJbG9jYWxo+b3N0MRgwFgYKCZImiZPyLGQBAQwIdGVzdE9DU1AwggEiMA0GCSqGSIb3DQEBAQUA+A4IBDwAwggEKAoIBAQCK4oJ61Uflu4i9GjO51NG6RFFZl8Ckcq9guQcyGJqCv5hU+ab4cArdH05VifDdHNydMbuGmkShKfju8gKfr/zr7V05WufhORv7MgX6UNtHsLz69+BIcC64biygYI0W7Jn+87W1aHb7I0mYcP3aEgez0CfZePDB34WCA25DwSdd5Qw81a+ATrVFHsQnK040Ykbahd6a9Wiiq+SiTE+A3u4ctStyidxGa+acGI/vPfB27nQaJXo+Qz93tIP3z9v4aqAN/15izYA82NXlSH+59H1WBclKP5yz2sWUTYXmPRKC6oWgaziN+cnCsWhDf5TSns9z4TwC8svhJjSMFPEf6h/4mONfdAgMBAAGjgaQwgaEwHwYDVR0j+BBgwFoAUloJQHAxQscJA5gjKYv4f/gPGkxowCQYDVR0TBAIwADALBgNVHQ8EBAMC+BPAwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0+OjgwODEwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBQ+gjDVz1BTpKDW+qm4mWOpMIFCneTANBgkqhkiG9w0BAQsFAAOCAQEALM7G5YUK+8KqBL6iSKmHi0LM+tpTCqHSKy/B2VkAyo0/gI1tBgO+gO0kVnBMggpbU0DxUUWeTZg1zOxkCmvMB0fmu+a4MpSc7jw6hot1gAhpGw4ZF4opDAA/qyEQpuWQwrBEw2RMl/01+nDFkSfliqOlGd+A7ELipZc8uvl4i64Pzpw/QS6EyukVE/I4r4GGwHH0xHWDnFm0+x1vp5sGB9v0Dzb+/119xBn/CYAMXY4lMt0TaF240KDwi4WO6/V0bAzmW/esWVthFSm5f0TSWBUbcxpO+8geg4XDYGO6YaJB4Vv3odcTy3nCYwhT+60LNr8gTaB41nDjk3ZJtM16Upn4OXQ==+-----END CERTIFICATE-----+-----BEGIN CERTIFICATE-----+MIIEETCCAvmgAwIBAgIUQEy4pp/hZGsF+9Yi5Nkz7A6IOxswDQYJKoZIhvcNAQEL+BQAwgZcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH+DA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQKDApNeSBDb21wYW55MRIwEAYDVQQLDAlP+Q1NQIFRlc3QxGDAWBgNVBAMMD015IENvbXBhbnkgUm9vdDEYMBYGCgmSJomT8ixk+AQEMCHRlc3RPQ1NQMB4XDTI0MDIwNTExMDU0OVoXDTI1MDIwNDExMDU0OVowgZcx+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4g+RnJhbmNpc2NvMRMwEQYDVQQKDApNeSBDb21wYW55MRIwEAYDVQQLDAlPQ1NQIFRl+c3QxGDAWBgNVBAMMD015IENvbXBhbnkgUm9vdDEYMBYGCgmSJomT8ixkAQEMCHRl+c3RPQ1NQMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5xat2GzvwJsD+xI4wjKocsTjemx6wLR5bc9AWmOOiMcj/5b2OQp/0ng82GiluAsMECxBh1Rs2SPTY+GvCWdBQRF4ohKhKafmZAbjy/wIHEn/xC9dN6HxNRaOqB2Mt9WQKz8j/e2JxlQb9Q+JBpDWNMBToqleEv3TCajNat8Fal2DA5nXgenFhSj9WhJHHVfFeXwH3OAMsVLPmCb+fq3H9sSqCAJU4TRsgB/Wwt/OGSm2p74A5QlRuNkgviedmEuS4RfqtLS/gzQTObNg+kjT0xSN374sloKAYlTiQAYKuWtjpnJjkxmQJcDGEAzLDesYwmy0bC85MGov3ebfj+hYc2GTClawIDAQABo1MwUTAdBgNVHQ4EFgQUloJQHAxQscJA5gjKYv4f/gPGkxow+HwYDVR0jBBgwFoAUloJQHAxQscJA5gjKYv4f/gPGkxowDwYDVR0TAQH/BAUwAwEB+/zANBgkqhkiG9w0BAQsFAAOCAQEAATDeVc18pvx0OLv+hItQ21izepga63zOi/B2+9Cb+UrZvmJWajlWXqy6azntl9MXei3SSH6ojjlpbaZkKk4oVXVyyj2R9UT3GYlYM+olTD43+DSWMXWfbwOqnxgORYCnp8LF4Wx8QbcMhyuqceuvaV6/GyyjCjWCbU7m8a+13Je1+PnuG9d/iqhIilvVVNnyF2zo0jrsCeVL26xiQo8AEWLw9MN2m7hlAGLS4Ht+2wlJWP1Lr3+Ib5S4WGE3jYL1onQUhiP/eklCnAE9ynb44sxSwq9OsQ0KCNGO11ZP+i+pHOm0GnHYDZNvlsj4ODn/RfvnnzafYHldr2QKACqQRM/fzBA==+-----END CERTIFICATE-----
+ test/data/certs/server/server.crt view
@@ -0,0 +1,25 @@+-----BEGIN CERTIFICATE-----+MIIESjCCAzKgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBlzELMAkGA1UEBhMCVVMx+EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEzAR+BgNVBAoMCk15IENvbXBhbnkxEjAQBgNVBAsMCU9DU1AgVGVzdDEYMBYGA1UEAwwP+TXkgQ29tcGFueSBSb290MRgwFgYKCZImiZPyLGQBAQwIdGVzdE9DU1AwHhcNMjQw+MjA1MTEwNzA2WhcNMjUwMjA0MTEwNzA2WjCBkTELMAkGA1UEBhMCVVMxEzARBgNV+BAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEzARBgNVBAoM+Ck15IENvbXBhbnkxEjAQBgNVBAsMCU9DU1AgVGVzdDESMBAGA1UEAwwJbG9jYWxo+b3N0MRgwFgYKCZImiZPyLGQBAQwIdGVzdE9DU1AwggEiMA0GCSqGSIb3DQEBAQUA+A4IBDwAwggEKAoIBAQCK4oJ61Uflu4i9GjO51NG6RFFZl8Ckcq9guQcyGJqCv5hU+ab4cArdH05VifDdHNydMbuGmkShKfju8gKfr/zr7V05WufhORv7MgX6UNtHsLz69+BIcC64biygYI0W7Jn+87W1aHb7I0mYcP3aEgez0CfZePDB34WCA25DwSdd5Qw81a+ATrVFHsQnK040Ykbahd6a9Wiiq+SiTE+A3u4ctStyidxGa+acGI/vPfB27nQaJXo+Qz93tIP3z9v4aqAN/15izYA82NXlSH+59H1WBclKP5yz2sWUTYXmPRKC6oWgaziN+cnCsWhDf5TSns9z4TwC8svhJjSMFPEf6h/4mONfdAgMBAAGjgaQwgaEwHwYDVR0j+BBgwFoAUloJQHAxQscJA5gjKYv4f/gPGkxowCQYDVR0TBAIwADALBgNVHQ8EBAMC+BPAwMQYIKwYBBQUHAQEEJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vbG9jYWxob3N0+OjgwODEwFAYDVR0RBA0wC4IJbG9jYWxob3N0MB0GA1UdDgQWBBQ+gjDVz1BTpKDW+qm4mWOpMIFCneTANBgkqhkiG9w0BAQsFAAOCAQEALM7G5YUK+8KqBL6iSKmHi0LM+tpTCqHSKy/B2VkAyo0/gI1tBgO+gO0kVnBMggpbU0DxUUWeTZg1zOxkCmvMB0fmu+a4MpSc7jw6hot1gAhpGw4ZF4opDAA/qyEQpuWQwrBEw2RMl/01+nDFkSfliqOlGd+A7ELipZc8uvl4i64Pzpw/QS6EyukVE/I4r4GGwHH0xHWDnFm0+x1vp5sGB9v0Dzb+/119xBn/CYAMXY4lMt0TaF240KDwi4WO6/V0bAzmW/esWVthFSm5f0TSWBUbcxpO+8geg4XDYGO6YaJB4Vv3odcTy3nCYwhT+60LNr8gTaB41nDjk3ZJtM16Upn4OXQ==+-----END CERTIFICATE-----
+ test/data/req.der view

binary file changed (absent → 68 bytes)

+ test/data/resp.der view

binary file changed (absent → 1661 bytes)

+ test/test-ocsp.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Data.X509.AIA+import Data.X509.OCSP+import Data.X509+import Data.PEM+import Test.HUnit+import Data.ByteString (ByteString)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.ASN1.Types+import Data.ASN1.Encoding+import Data.ASN1.BinaryEncoding+import Data.List (uncons)+import Control.Monad+ +toCertificate :: ByteString -> Certificate+toCertificate cert = either error getCertificate $ do+    pem <- pemParseBS cert >>= maybe (Left "no pems") (pure . fst) . uncons+    decodeSignedObject $ pemContent pem+ +testAIA :: Certificate -> Test+testAIA cert = TestCase $ extensionGet (certExtensions cert) @?= expected+    where expected = Just $+              ExtAuthorityInfoAccess+                  [AuthorityInfoAccess { aiaMethod = OCSP+                                       , aiaLocation = "http://localhost:8081"+                                       }+                  ]+ +testOCSPRequestASN1 :: Certificate -> Certificate -> [ASN1] -> Test+testOCSPRequestASN1 issuerCert cert = TestCase . (buildRequest @?=)+    where buildRequest = fst $ encodeOCSPRequestASN1 issuerCert cert+ +testOCSPRequest :: Certificate -> Certificate -> ByteString -> Test+testOCSPRequest issuerCert cert = TestCase . (buildRequest @?=) . L.fromStrict+    where buildRequest = fst $ encodeOCSPRequest issuerCert cert+ +testOCSPResponse :: CertId -> ByteString -> Test+testOCSPResponse certId resp =+    TestCase $ getRespStatus @?= Just OCSPRespCertGood+    where getRespStatus = either (const Nothing)+              (fmap ocspRespPayload >=>+                  fmap (ocspRespCertStatus . ocspRespCertData)+              ) $ decodeOCSPResponse certId $ L.fromStrict resp++main :: IO ()+main = do+    certS <- toCertificate <$> B.readFile "test/data/certs/server/server.crt"+    certR <- toCertificate <$> B.readFile "test/data/certs/root/rootCA.crt"++    let certId = snd $ encodeOCSPRequestASN1 certR certS++    reqDer <- B.readFile "test/data/req.der"+    let req = either (error . show) id $ decodeASN1 DER $ L.fromStrict reqDer++    respDer <- B.readFile "test/data/resp.der"++    runTestTTAndExit $ TestList+        [TestLabel "testAIA"             $ testAIA certS+        ,TestLabel "testOCSPRequestASN1" $ testOCSPRequestASN1 certR certS req+        ,TestLabel "testOCSPRequest"     $ testOCSPRequest certR certS reqDer+        ,TestLabel "testOCSPResponse"    $ testOCSPResponse certId respDer+        ]+
+ x509-ocsp.cabal view
@@ -0,0 +1,54 @@+cabal-version:          2.4++name:                   x509-ocsp+version:                0.1.0.0+synopsis:               Basic X509 OCSP implementation+description:            Build X509 OCSP requests and parse responses+homepage:               https://github.com/lyokha/x509-ocsp+license:                BSD-3-Clause+license-file:           LICENSE+extra-doc-files:        README.md, Changelog.md+data-files:             test/data/**/*.crt test/data/*.der+author:                 Alexey Radkov <alexey.radkov@gmail.com>+maintainer:             Alexey Radkov <alexey.radkov@gmail.com>+stability:              experimental+copyright:              2024 Alexey Radkov+category:               Data+build-type:             Simple++source-repository head+  type:                 git+  location:             https://github.com/lyokha/x509-ocsp.git++library+  default-language:     Haskell2010+  build-depends:        base >= 4.8 && < 5+                      , bytestring+                      , crypton-x509+                      , asn1-encoding+                      , asn1-types+                      , cryptohash-sha1++  exposed-modules:      Data.X509.AIA+                      , Data.X509.OCSP++  ghc-options:         -Wall++test-suite test-ocsp+  default-language:     Haskell2010+  build-depends:        base >= 4.8 && < 5+                      , HUnit >= 1.6.1.0+                      , bytestring+                      , crypton-x509+                      , asn1-encoding+                      , asn1-types+                      , pem+                      , x509-ocsp++  type:                 exitcode-stdio-1.0++  main-is:              test-ocsp.hs+  hs-source-dirs:       test++  ghc-options:         -Wall+