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 +4/−0
- Data/X509/AIA.hs +92/−0
- Data/X509/OCSP.hs +241/−0
- LICENSE +25/−0
- README.md +89/−0
- Setup.hs +3/−0
- test/data/certs/root/rootCA.crt +24/−0
- test/data/certs/server-chain.crt +49/−0
- test/data/certs/server/server.crt +25/−0
- test/data/req.der binary
- test/data/resp.der binary
- test/test-ocsp.hs +67/−0
- x509-ocsp.cabal +54/−0
+ 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+=========================================++[](https://github.com/lyokha/x509-ocsp/actions?query=workflow%3ACI)+[](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+