data-rfc5280 (empty) → 0.1.0.0
raw patch · 36 files changed
+2451/−0 lines, 36 filesdep +QuickCheckdep +basedep +bytestringsetup-changed
Dependencies added: QuickCheck, base, bytestring, data-rfc5280, email-validate, hspec, ip, modern-uri, nonempty-containers, text
Files
- ChangeLog.md +9/−0
- LICENSE +30/−0
- Setup.hs +4/−0
- data-rfc5280.cabal +100/−0
- src/Data/Rfc5280.hs +167/−0
- src/Data/Rfc5280/Assert.hs +34/−0
- src/Data/Rfc5280/AuthorityInfoAccess.hs +62/−0
- src/Data/Rfc5280/AuthorityKeyIdentifier.hs +75/−0
- src/Data/Rfc5280/BasicConstraints.hs +69/−0
- src/Data/Rfc5280/CRLDistributionPoints.hs +67/−0
- src/Data/Rfc5280/CertificatePolicies.hs +39/−0
- src/Data/Rfc5280/ExtKeyUsage.hs +72/−0
- src/Data/Rfc5280/GeneralName.hs +313/−0
- src/Data/Rfc5280/HasOID.hs +48/−0
- src/Data/Rfc5280/InhibitAnyPolicy.hs +54/−0
- src/Data/Rfc5280/Internal.hs +79/−0
- src/Data/Rfc5280/IssuerAltName.hs +41/−0
- src/Data/Rfc5280/KeyUsage.hs +92/−0
- src/Data/Rfc5280/NameConstraints.hs +63/−0
- src/Data/Rfc5280/PolicyMappings.hs +65/−0
- src/Data/Rfc5280/SubjectAltName.hs +41/−0
- src/Data/Rfc5280/SubjectKeyIdentifier.hs +43/−0
- test/Rfc5280/AssertSpec.hs +30/−0
- test/Rfc5280/AuthorityInfoAccessSpec.hs +54/−0
- test/Rfc5280/CRLDistributionPointsSpec.hs +65/−0
- test/Rfc5280/Fixtures.hs +38/−0
- test/Rfc5280/GeneralNameSpec.hs +108/−0
- test/Rfc5280/Generators.hs +71/−0
- test/Rfc5280/HasOIDSpec.hs +57/−0
- test/Rfc5280/InhibitAnyPolicySpec.hs +31/−0
- test/Rfc5280/IssuerAltNameSpec.hs +64/−0
- test/Rfc5280/NameConstraintsSpec.hs +62/−0
- test/Rfc5280/PolicyMappingsSpec.hs +37/−0
- test/Rfc5280/SubjectAltNameSpec.hs +64/−0
- test/Rfc5280Spec.hs +157/−0
- test/Spec.hs +46/−0
+ ChangeLog.md view
@@ -0,0 +1,9 @@+# Revision history for data-rfc5280++`data-rfc5280` uses [PVP Versioning][1].++## 0.1.0.0 -- 2026-09-05++* Initial version.++[1]: https://pvp.haskell.org
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2026, Tim Emiola++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 Tim Emiola 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.
+ Setup.hs view
@@ -0,0 +1,4 @@+import Distribution.Simple+++main = defaultMain
+ data-rfc5280.cabal view
@@ -0,0 +1,100 @@+cabal-version: 3.0+name: data-rfc5280+version: 0.1.0.0+synopsis: Represent the standard X.509v3 certificate extensions+description:+ @data-rfc5280@ provides Haskell types for the standard X.509v3+ certificate extensions defined in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)++ Extensions covered include @BasicConstraints@, @KeyUsage@,+ @ExtendedKeyUsage@, @SubjectAltName@, @IssuerAltName@,+ @AuthorityKeyIdentifier@, @SubjectKeyIdentifier@, @CertificatePolicies@,+ @PolicyMappings@, @NameConstraints@, @CRLDistributionPoints@,+ @AuthorityInfoAccess@, and @InhibitAnyPolicy@.++ The package also provides typeclasses that support serialisation to to OpenSSL configuration format++ See the [README](https://github.com/adetokunbo/data-rfc5280/blob/main/README.md) for more details++license: BSD-3-Clause+license-file: LICENSE+author: Tim Emiola+maintainer: adetokunbo@emio.la+category: Cryptography+homepage: https://github.com/adetokunbo/data-rfc5280#readme+bug-reports:+ https://github.com/adetokunbo/data-rfc5280/issues+tested-with: GHC ==8.10.7 || ==9.0.2 || ==9.2.8 || ==9.4.8 || ==9.6.6 || ==9.8.4 || ==9.10.1 || ==9.12.1+build-type: Simple+extra-source-files:+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/adetokunbo/data-rfc5280.git++library+ exposed-modules:+ Data.Rfc5280+ Data.Rfc5280.Assert+ Data.Rfc5280.AuthorityInfoAccess+ Data.Rfc5280.AuthorityKeyIdentifier+ Data.Rfc5280.BasicConstraints+ Data.Rfc5280.CertificatePolicies+ Data.Rfc5280.CRLDistributionPoints+ Data.Rfc5280.ExtKeyUsage+ Data.Rfc5280.GeneralName+ Data.Rfc5280.HasOID+ Data.Rfc5280.InhibitAnyPolicy+ Data.Rfc5280.IssuerAltName+ Data.Rfc5280.PolicyMappings+ Data.Rfc5280.KeyUsage+ Data.Rfc5280.NameConstraints+ Data.Rfc5280.SubjectAltName+ Data.Rfc5280.SubjectKeyIdentifier+ other-modules:+ Data.Rfc5280.Internal+ hs-source-dirs: src+ build-depends:+ , base >=4.10 && <5+ , bytestring >=0.10.8.2 && <0.11 || >=0.11.3.1 && <0.13+ , email-validate >=2.3 && <2.4+ , ip >=1.3 && <1.9+ , modern-uri >=0.3 && <0.4+ , nonempty-containers >=0.3 && <0.4+ , text >=1.2 && <2.2++ default-language: Haskell2010+ ghc-options: -Wall -Wincomplete-uni-patterns -Wpartial-fields -fwarn-tabs++test-suite test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: test+ other-modules:+ Rfc5280Spec+ Rfc5280.AssertSpec+ Rfc5280.AuthorityInfoAccessSpec+ Rfc5280.CRLDistributionPointsSpec+ Rfc5280.Fixtures+ Rfc5280.GeneralNameSpec+ Rfc5280.Generators+ Rfc5280.HasOIDSpec+ Rfc5280.InhibitAnyPolicySpec+ Rfc5280.IssuerAltNameSpec+ Rfc5280.NameConstraintsSpec+ Rfc5280.PolicyMappingsSpec+ Rfc5280.SubjectAltNameSpec+ default-language: Haskell2010+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -fwarn-tabs+ build-depends:+ , base+ , bytestring+ , email-validate >=2.3 && <2.4+ , hspec >=2.7 && < 2.12+ , ip >=1.3 && <1.9+ , modern-uri >=0.3 && <0.4+ , QuickCheck >=2.13 && < 2.16+ , text >=1.2 && <2.2+ , data-rfc5280+
+ src/Data/Rfc5280.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PatternSynonyms #-}++{- |+Module : Data.Rfc5280+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Represent the standard X.509v3 certificate extensions from RFC 5280.+Each section below provides constructors for one or more extension types;+'renderConfig' serialises any of them to OpenSSL configuration format.+-}+module Data.Rfc5280+ ( -- * Extension types+ BasicConstraints (..)+ , BasicConstraintsError (..)+ , mkBasicConstraints+ , KeyUsageBit (..)+ , KeyUsage+ , ExtKeyUsagePurpose (..)+ , ExtKeyUsage+ , SubjectKeyIdentifier (..)+ , AuthorityKeyIdentifier (..)+ , mkAuthorityKeyIdentifier+ , CertificatePolicies (..)+ , mkCertificatePolicies++ -- * OID construction+ , OID+ , OIDError (..)+ , mkOID++ -- * General names+ , GeneralName (DNS, IPAddr, EmailAddr, URIName, Other)+ , pattern RegisteredID+ , DnsName+ , dnsNameText+ , OtherName+ , pattern OtherName+ , onTypeId+ , onEncoding+ , onValue+ , Asn1StringType (..)+ , DNSNameError (..)+ , OtherNameError (..)+ , mkDnsName+ , mkDnsConstraint+ , mkRegisteredID+ , mkOtherName+ , mkOther++ -- * Alt-name extensions+ , SubjectAltName (..)+ , mkSubjectAltName+ , IssuerAltName (..)+ , mkIssuerAltName++ -- * CRL extensions+ , CRLDistributionPoints (..)+ , mkCRLDistributionPoints+ , DistributionPoint (..)+ , mkDistributionPoint++ -- * Name constraints+ , NameConstraints (..)+ , mkNameConstraints+ , NameConstraint (..)++ -- * Policy extensions+ , InhibitAnyPolicy (..)+ , InhibitAnyPolicyError (..)+ , mkInhibitAnyPolicy+ , PolicyMappings (..)+ , mkPolicyMappings+ , PolicyMapping (..)+ , mkPolicyMapping++ -- * Criticality wrapper and OID lookup+ , Extension (..)+ , HasOID (..)++ -- * Render to OpenSSL config format+ , RenderConfig (..)+ , renderConfig++ -- * Lifting results into MonadIO+ , assertRight+ , assertJust++ -- * Re-exported for convenience++ -- | 'NonEmpty' from "Data.List.NonEmpty"; 'fromList' from "Data.Set.NonEmpty".+ , fromList+ , NonEmpty (..)+ )+where++import Data.ByteString (ByteString)+import Data.ByteString.Builder (toLazyByteString)+import qualified Data.ByteString.Lazy as LBS+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.Assert (assertJust, assertRight)+import Data.Rfc5280.AuthorityKeyIdentifier+ ( AuthorityKeyIdentifier (..)+ , mkAuthorityKeyIdentifier+ )+import Data.Rfc5280.BasicConstraints+ ( BasicConstraints (..)+ , BasicConstraintsError (..)+ , mkBasicConstraints+ )+import Data.Rfc5280.CRLDistributionPoints+ ( CRLDistributionPoints (..)+ , DistributionPoint (..)+ , mkCRLDistributionPoints+ , mkDistributionPoint+ )+import Data.Rfc5280.CertificatePolicies+ ( CertificatePolicies (..)+ , mkCertificatePolicies+ )+import Data.Rfc5280.ExtKeyUsage (ExtKeyUsage, ExtKeyUsagePurpose (..))+import Data.Rfc5280.GeneralName+ ( Asn1StringType (..)+ , DNSNameError (..)+ , DnsName+ , GeneralName (DNS, EmailAddr, IPAddr, Other, URIName)+ , OtherName+ , OtherNameError (..)+ , dnsNameText+ , mkDnsConstraint+ , mkDnsName+ , mkOther+ , mkOtherName+ , mkRegisteredID+ , onEncoding+ , onTypeId+ , onValue+ , pattern OtherName+ , pattern RegisteredID+ )+import Data.Rfc5280.HasOID (Extension (..), HasOID (..))+import Data.Rfc5280.InhibitAnyPolicy (InhibitAnyPolicy (..), InhibitAnyPolicyError (..), mkInhibitAnyPolicy)+import Data.Rfc5280.Internal (OID, OIDError (..), RenderConfig (..), mkOID)+import Data.Rfc5280.IssuerAltName (IssuerAltName (..), mkIssuerAltName)+import Data.Rfc5280.KeyUsage (KeyUsage, KeyUsageBit (..))+import Data.Rfc5280.NameConstraints+ ( NameConstraint (..)+ , NameConstraints (..)+ , mkNameConstraints+ )+import Data.Rfc5280.PolicyMappings+ ( PolicyMapping (..)+ , PolicyMappings (..)+ , mkPolicyMapping+ , mkPolicyMappings+ )+import Data.Rfc5280.SubjectAltName (SubjectAltName (..), mkSubjectAltName)+import Data.Rfc5280.SubjectKeyIdentifier (SubjectKeyIdentifier (..))+import Data.Set.NonEmpty (fromList)+++-- | Render an extension value as a strict 'ByteString' in OpenSSL configuration format.+renderConfig :: (RenderConfig a) => a -> ByteString+renderConfig = LBS.toStrict . toLazyByteString . renderBuilder
+ src/Data/Rfc5280/Assert.hs view
@@ -0,0 +1,34 @@+{- |+Module : Data.Rfc5280.Assert+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Helpers for lifting RFC 5280 smart-constructor results into 'MonadIO'.++'assertRight' and 'assertJust' convert the two common failure containers+('Either' and 'Maybe') into any 'MonadIO' context, throwing a 'userError'+when the value is absent.+-}+module Data.Rfc5280.Assert+ ( assertRight+ , assertJust+ )+where++import Control.Exception (throwIO)+import Control.Monad.IO.Class (MonadIO, liftIO)+++{- | Lift an @Either e a@ into any 'MonadIO', throwing a 'userError' with+'show' of the error on 'Left'.+-}+assertRight :: (Show e, MonadIO m) => Either e a -> m a+assertRight = either (liftIO . throwIO . userError . show) pure+++{- | Lift a @Maybe a@ into any 'MonadIO', throwing a 'userError' with the+given message on 'Nothing'.+-}+assertJust :: (MonadIO m) => String -> Maybe a -> m a+assertJust msg = maybe (liftIO . throwIO . userError $ msg) pure
+ src/Data/Rfc5280/AuthorityInfoAccess.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.AuthorityInfoAccess+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'AuthorityInfoAccess', representing the X.509 Authority Information+Access extension (RFC 5280 §4.2.2.1).+-}+module Data.Rfc5280.AuthorityInfoAccess+ ( AuthorityInfoAccess (..)+ , mkAuthorityInfoAccess+ , AccessDescription (..)+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (GeneralName)+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | A single access point within an 'AuthorityInfoAccess' extension.++The 'GeneralName' location is almost always a 'URIName' in practice, but+any variant is permitted by RFC 5280.+-}+data AccessDescription+ = -- | An OCSP responder location. Rendered as @OCSP;\<location\>@.+ OCSP !GeneralName+ | -- | A CA issuers location. Rendered as @caIssuers;\<location\>@.+ CAIssuers !GeneralName+ deriving (Eq, Show)+++{- | Represents the AuthorityInfoAccess extension (RFC 5280 §4.2.2.1).++Contains one or more 'AccessDescription' values. Use 'mkAuthorityInfoAccess'+to construct a value.+-}+newtype AuthorityInfoAccess = AuthorityInfoAccess (NonEmpty AccessDescription)+ deriving (Eq, Show)+++-- | Construct an 'AuthorityInfoAccess' from one or more 'AccessDescription' values.+mkAuthorityInfoAccess :: AccessDescription -> [AccessDescription] -> AuthorityInfoAccess+mkAuthorityInfoAccess x xs = AuthorityInfoAccess (x :| xs)+++instance HasOID AuthorityInfoAccess where+ extensionOID _ = 1 :| [3, 6, 1, 5, 5, 7, 1, 1]+++instance RenderConfig AccessDescription where+ renderBuilder (OCSP loc) = "OCSP;" <> renderBuilder loc+ renderBuilder (CAIssuers loc) = "caIssuers;" <> renderBuilder loc+++instance RenderConfig AuthorityInfoAccess where+ renderBuilder (AuthorityInfoAccess xs) = intersperseCommas (fmap renderBuilder xs)
+ src/Data/Rfc5280/AuthorityKeyIdentifier.hs view
@@ -0,0 +1,75 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.AuthorityKeyIdentifier+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'AuthorityKeyIdentifier' extension type and its smart constructor.+-}+module Data.Rfc5280.AuthorityKeyIdentifier+ ( AuthorityKeyIdentifier (..)+ , mkAuthorityKeyIdentifier+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | Represents the AuthorityKeyIdentifier extension++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1 RFC 5280 §4.2.1.1>.+-}+data AuthorityKeyIdentifier = AuthorityKeyIdentifier+ { akiKeyId :: !Bool+ -- ^ include key ID+ , akiKeyIdAlways :: !Bool+ {- ^ always include the key identifier, even if the issuer certificate+ has no SubjectKeyIdentifier extension+ -}+ , akiIssuer :: !Bool+ -- ^ include issuer name + serial+ , akiIssuerAlways :: !Bool+ {- ^ always include the issuer name and serial, even if the issuer certificate+ has no SubjectKeyIdentifier extension+ -}+ }+ deriving (Eq, Show)+++{- | Construct an 'AuthorityKeyIdentifier', normalising the @always@ flags:+if @keyIdAlways@ is 'True', @keyId@ is set to 'True'; if @issuerAlways@ is+'True', @issuer@ is set to 'True'.+-}+mkAuthorityKeyIdentifier :: Bool -> Bool -> Bool -> Bool -> AuthorityKeyIdentifier+mkAuthorityKeyIdentifier keyId keyIdAlways issuer issuerAlways =+ AuthorityKeyIdentifier+ { akiKeyId = keyId || keyIdAlways+ , akiKeyIdAlways = keyIdAlways+ , akiIssuer = issuer || issuerAlways+ , akiIssuerAlways = issuerAlways+ }+++instance HasOID AuthorityKeyIdentifier where+ extensionOID _ = 2 :| [5, 29, 35]+++instance RenderConfig AuthorityKeyIdentifier where+ renderBuilder aki =+ let keyId+ | akiKeyIdAlways aki = Just "keyid:always"+ | akiKeyId aki = Just "keyid"+ | otherwise = Nothing+ issuer+ | akiIssuerAlways aki = Just "issuer:always"+ | akiIssuer aki = Just "issuer"+ | otherwise = Nothing+ in case (keyId, issuer) of+ (Nothing, Nothing) -> mempty+ (Nothing, Just x) -> x+ (Just x, Nothing) -> x+ (Just x, Just y) -> intersperseCommas (x :| [y])
+ src/Data/Rfc5280/BasicConstraints.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.BasicConstraints+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+++Provides the 'BasicConstraints' extension type and its smart constructor.+-}+module Data.Rfc5280.BasicConstraints+ ( BasicConstraints (..)+ , BasicConstraintsError (..)+ , mkBasicConstraints+ )+where++import Data.ByteString.Builder (intDec)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..))+++{- | Represents the basic constraints extension++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 RFC 5280 §4.2.1.9>.+-}+data BasicConstraints+ = BasicConstraints+ { bcIsCA :: !Bool+ -- ^ is the subject of the certificate a certificate authority+ , bcPathLength :: !(Maybe Int)+ {- ^ the maximum number of non-self-issued intermediate certificates+ that may follow this certificate in the path; only meaningful when+ 'bcIsCA' is 'True'+ -}+ }+ deriving (Eq, Show)+++-- | Failure modes for 'mkBasicConstraints'.+data BasicConstraintsError+ = -- | @pathLenConstraint@ is present but @cA@ is @FALSE@.+ PathLenWithoutCA+ deriving (Eq, Show)+++{- | Construct a 'BasicConstraints', validating that @pathLenConstraint@ is+absent when @cA@ is @FALSE@ (RFC 5280 §4.2.1.9).+-}+mkBasicConstraints :: Bool -> Maybe Int -> Either BasicConstraintsError BasicConstraints+mkBasicConstraints False (Just _) = Left PathLenWithoutCA+mkBasicConstraints isCA pathLen = Right $ BasicConstraints{bcIsCA = isCA, bcPathLength = pathLen}+++instance HasOID BasicConstraints where+ extensionOID _ = 2 :| [5, 29, 19]+++instance RenderConfig BasicConstraints where+ renderBuilder bc =+ let BasicConstraints{bcIsCA, bcPathLength} = bc+ bcPrefix = "CA:"+ bcSuffix = if bcIsCA then "TRUE" else "FALSE"+ withPathLen x = ",pathLen" <> intDec x+ pathLen = maybe "" withPathLen bcPathLength+ in (bcPrefix <> bcSuffix <> pathLen)
+ src/Data/Rfc5280/CRLDistributionPoints.hs view
@@ -0,0 +1,67 @@+{- |+Module : Data.Rfc5280.CRLDistributionPoints+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'CRLDistributionPoints', representing the X.509 CRL Distribution+Points extension (RFC 5280 §4.2.1.13).++Only simple distribution points are modelled: each point carries a single+full-name 'GeneralName' location. The @reasons@, @cRLIssuer@, and+@nameRelativeToCRLIssuer@ fields from the RFC are not yet supported; those+require section-based OpenSSL config syntax which this library does not produce.+-}+module Data.Rfc5280.CRLDistributionPoints+ ( CRLDistributionPoints (..)+ , mkCRLDistributionPoints+ , DistributionPoint (..)+ , mkDistributionPoint+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (GeneralName)+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | A single CRL distribution point, identified by its full-name location.++In the OpenSSL config format, each 'DistributionPoint' renders as the+'GeneralName' of its location. In practice the location is almost always+a 'URIName'.+-}+newtype DistributionPoint = DistributionPoint GeneralName+ deriving (Eq, Show)+++{- | Represents the CRLDistributionPoints extension (RFC 5280 §4.2.1.13).++Contains one or more 'DistributionPoint' values. Use 'mkCRLDistributionPoints'+to construct a value.+-}+newtype CRLDistributionPoints = CRLDistributionPoints (NonEmpty DistributionPoint)+ deriving (Eq, Show)+++-- | Construct a 'DistributionPoint' from a 'GeneralName' full-name location.+mkDistributionPoint :: GeneralName -> DistributionPoint+mkDistributionPoint = DistributionPoint+++-- | Construct a 'CRLDistributionPoints' from one or more 'DistributionPoint' values.+mkCRLDistributionPoints :: DistributionPoint -> [DistributionPoint] -> CRLDistributionPoints+mkCRLDistributionPoints x xs = CRLDistributionPoints (x :| xs)+++instance HasOID CRLDistributionPoints where+ extensionOID _ = 2 :| [5, 29, 31]+++instance RenderConfig DistributionPoint where+ renderBuilder (DistributionPoint name) = renderBuilder name+++instance RenderConfig CRLDistributionPoints where+ renderBuilder (CRLDistributionPoints pts) = intersperseCommas (fmap renderBuilder pts)
+ src/Data/Rfc5280/CertificatePolicies.hs view
@@ -0,0 +1,39 @@+{- |+Module : Data.Rfc5280.CertificatePolicies+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'CertificatePolicies' extension type and its smart constructor.+-}+module Data.Rfc5280.CertificatePolicies+ ( CertificatePolicies (..)+ , mkCertificatePolicies+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (OID, RenderConfig (..), intersperseCommas, oidBuilder)+++{- | Represents @CertificatePolicies@ (RFC 5280 §4.2.1.4).++Only the policy OIDs are modelled. @PolicyQualifierInfo@ entries (CPS URIs,+user notices) are not yet supported.+-}+newtype CertificatePolicies = CertificatePolicies (NonEmpty OID)+ deriving (Eq, Show)+++-- | Construct @CertificatePolicies@ from a non-empty sequence of OIDs.+mkCertificatePolicies :: OID -> [OID] -> CertificatePolicies+mkCertificatePolicies x xs = CertificatePolicies $ x :| xs+++instance HasOID CertificatePolicies where+ extensionOID _ = 2 :| [5, 29, 32]+++instance RenderConfig CertificatePolicies where+ renderBuilder (CertificatePolicies xs) = intersperseCommas $ fmap oidBuilder xs
+ src/Data/Rfc5280/ExtKeyUsage.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.ExtKeyUsage+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'ExtKeyUsage' extension type.+-}+module Data.Rfc5280.ExtKeyUsage+ ( ExtKeyUsagePurpose (..)+ , ExtKeyUsage+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+import qualified Data.Set.NonEmpty as NES+++{- | Represents the bits that can set for @ExtKeyUsage@++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12 RFC 5280 §4.2.1.12>.+-}+data ExtKeyUsagePurpose+ = -- | TLS WWW server authentication. Renders as @\"serverAuth\"@.+ ServerAuth+ | -- | TLS WWW client authentication. Renders as @\"clientAuth\"@.+ ClientAuth+ | -- | Signing of downloadable executable code. Renders as @\"codeSigning\"@.+ CodeSigning+ | -- | Email protection (S\/MIME). Renders as @\"emailProtection\"@.+ EmailProtection+ | {- | Binding an object hash to a trusted time source. Renders as+ @\"timeStamping\"@.+ -}+ TimeStamping+ | -- | Signing OCSP responses. Renders as @\"OCSPSigning\"@ (capital OCSP).+ OCSPSigning+ | {- | Permits any extended key usage purpose. Renders as+ @\"anyExtendedKeyUsage\"@ (lowercase @any@).+ -}+ AnyExtendedKeyUsage+ deriving (Eq, Show, Ord, Enum, Bounded)+++instance RenderConfig ExtKeyUsagePurpose where+ renderBuilder ServerAuth = "serverAuth"+ renderBuilder ClientAuth = "clientAuth"+ renderBuilder CodeSigning = "codeSigning"+ renderBuilder EmailProtection = "emailProtection"+ renderBuilder TimeStamping = "timeStamping"+ renderBuilder OCSPSigning = "OCSPSigning"+ renderBuilder AnyExtendedKeyUsage = "anyExtendedKeyUsage"+++{- | Represents the @ExtKeyUsage@ extension++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12 RFC 5280 §4.2.1.12>.+-}+type ExtKeyUsage = NES.NESet ExtKeyUsagePurpose+++instance HasOID ExtKeyUsage where+ extensionOID _ = 2 :| [5, 29, 37]+++instance RenderConfig ExtKeyUsage where+ renderBuilder = intersperseCommas . fmap renderBuilder . NES.toList
+ src/Data/Rfc5280/GeneralName.hs view
@@ -0,0 +1,313 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PatternSynonyms #-}++{- |+Module : Data.Rfc5280.GeneralName+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'GeneralName', the common name type used across several X.509+extensions, including SubjectAltName and AuthorityInfoAccess.++'DirectoryName', x400Address, and ediPartyName are not yet modelled.+-}+module Data.Rfc5280.GeneralName+ ( GeneralName (DNS, IPAddr, EmailAddr, URIName, Other)+ , pattern RegisteredID+ , DnsName+ , dnsNameText+ , OtherName+ , pattern OtherName+ , onTypeId+ , onEncoding+ , onValue+ , Asn1StringType (..)+ , DNSNameError (..)+ , OtherNameError (..)+ , mkDnsName+ , mkDnsConstraint+ , mkRegisteredID+ , mkOtherName+ , mkOther+ )+where++import Data.Bifunctor (first)+import Data.ByteString.Builder (byteString)+import Data.Char (isAlphaNum, isAscii)+import Data.Rfc5280.Internal (OID, OIDError, RenderConfig (..), mkOID, oidBuilder)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import Net.IP (IP)+import qualified Net.IP as IP+import Text.Email.Validate (EmailAddress)+import qualified Text.Email.Validate as Email+import Text.URI (URI)+import qualified Text.URI as URI+++{- | A validated DNS hostname.++The constructor is not exported; use 'mkDnsName' for standard hostnames or+'mkDnsConstraint' for name-constraint subtree names (which may have a leading+dot such as @\".example.com\"@).+-}+newtype DnsName = DnsName Text+ deriving (Eq, Show)+++-- | Extract the underlying 'Text' from a 'DnsName'.+dnsNameText :: DnsName -> Text+dnsNameText (DnsName t) = t+++{- | A general name as defined in RFC 5280 §4.1.2.6.++Used in extensions such as SubjectAltName and AuthorityInfoAccess.+-}+data GeneralName+ = -- | A DNS hostname. Rendered as @DNS:\<name\>@. Construct via 'mkDnsName'.+ DNS !DnsName+ | -- | An IPv4 or IPv6 address. Rendered as @IP:\<address\>@.+ IPAddr !IP+ | -- | An email address. Rendered as @email:\<address\>@.+ EmailAddr !EmailAddress+ | -- | A URI. Rendered as @URI:\<uri\>@.+ URIName !URI+ | -- Internal constructor; exposed for matching via 'pattern RegisteredID'.+ RegisteredID_ !OID+ | {- | An arbitrary other name. Rendered as+ @otherName:\<oid\>;\<type\>:\<value\>@, where @\<type\>@ is the+ OpenSSL tag string for the 'Asn1StringType'.+ -}+ Other !OtherName+ deriving (Eq, Show)+++{- | Match a 'GeneralName' carrying an ASN.1 registered object identifier.++This is a unidirectional pattern: it can be used in pattern matches but not+to construct a 'GeneralName'. Use 'mkRegisteredID' to construct one, which+validates the OID arcs via 'mkOID'.+-}+pattern RegisteredID :: OID -> GeneralName+pattern RegisteredID o <- RegisteredID_ o+++{-# COMPLETE DNS, IPAddr, EmailAddr, URIName, RegisteredID, Other #-}+++{- | The fields of an @otherName@ general name (RFC 5280 §4.1.2.6).++Carries the type OID, the ASN.1 string encoding, and the string value.+The 'MkOtherName' constructor is not exported; use 'mkOtherName' to construct+a value and 'pattern OtherName' to match one.+-}+data OtherName = MkOtherName+ { onTypeId :: !OID+ -- ^ OID identifying the name type.+ , onEncoding :: !Asn1StringType+ -- ^ ASN.1 string encoding for the value.+ , onValue :: !Text+ -- ^ The string value.+ }+ deriving (Eq, Show)+++{- | Match an 'OtherName' value.++This is a unidirectional pattern: it can be used in pattern matches but not+to construct an 'OtherName'. Use 'mkOtherName' to construct one, which+validates the OID arcs and checks the value against the declared encoding.+-}+pattern OtherName :: OID -> Asn1StringType -> Text -> OtherName+pattern OtherName oid enc val <-+ MkOtherName{onTypeId = oid, onEncoding = enc, onValue = val}+++{-# COMPLETE OtherName #-}+++{- | The ASN.1 string encoding for an 'OtherName' value.++Covers the four types commonly used in practice for X.509 extension values.+The ASN.1 standard defines additional string types — including+'VisibleString', 'UniversalString', 'TeletexString', 'NumericString', and+'GeneralString' — but these are not modelled here because they are either+legacy types or primarily relevant to distinguished name components rather+than extension values:++* 'TeletexString' and 'UniversalString' appeared in early X.509 DN fields+ but are discouraged by RFC 5280 and superseded by 'UTF8String'.+* 'NumericString' is for digit-only fields such as the @serialNumber@ DN+ attribute; it has no common use in extension values.+* 'VisibleString' and 'GeneralString' appear only in edge cases and legacy+ structures.++For the vast majority of 'OtherName' use cases — including Microsoft UPN+(which uses 'UTF8String') and email addresses (which use 'IA5String') — the+four modelled constructors are sufficient.+-}+data Asn1StringType+ = -- | UTF-8 encoding. Rendered as @UTF8@.+ UTF8String+ | -- | ASCII (IA5) encoding. Rendered as @IA5@.+ IA5String+ | -- | PrintableString encoding. Rendered as @PRINTABLE@.+ PrintableString+ | -- | BMP (UCS-2) encoding. Rendered as @BMP@.+ BMPString+ deriving (Eq, Show)+++instance RenderConfig Asn1StringType where+ renderBuilder UTF8String = "UTF8"+ renderBuilder IA5String = "IA5"+ renderBuilder PrintableString = "PRINTABLE"+ renderBuilder BMPString = "BMP"+++instance RenderConfig GeneralName where+ renderBuilder (DNS (DnsName t)) = "DNS:" <> byteString (TE.encodeUtf8 t)+ renderBuilder (IPAddr ip) = "IP:" <> byteString (TE.encodeUtf8 (IP.encode ip))+ renderBuilder (EmailAddr addr) = "email:" <> byteString (Email.toByteString addr)+ renderBuilder (URIName uri) = "URI:" <> byteString (TE.encodeUtf8 (URI.render uri))+ renderBuilder (RegisteredID_ o) = "RID:" <> oidBuilder o+ renderBuilder (Other on) =+ "otherName:"+ <> oidBuilder (onTypeId on)+ <> ";"+ <> renderBuilder (onEncoding on)+ <> ":"+ <> byteString (TE.encodeUtf8 (onValue on))+++-- | Failure modes for 'mkDnsName' and 'mkDnsConstraint'.+data DNSNameError+ = -- | The name or a label within it is empty.+ NameEmpty+ | -- | The name exceeds 253 characters.+ NameTooLong+ | -- | A label exceeds 63 characters.+ LabelTooLong+ | -- | A label starts with a hyphen.+ LabelLeadingHyphen+ | -- | A label ends with a hyphen.+ LabelTrailingHyphen+ | -- | A label contains a character outside @[A-Za-z0-9-]@.+ LabelInvalidChar+ deriving (Eq, Show)+++{- | Construct a 'DnsName', validating against RFC 1123 hostname rules.++Returns @Left@ with a 'DNSNameError' if the name is invalid. Accepts a+wildcard @*@ as the first label (e.g. @\"*.example.com\"@). IDNA\/punycode+encoding of Unicode hostnames must be done by the caller before passing to+this function.++To construct a name-constraint subtree name with a leading dot (e.g.+@\".example.com\"@), use 'mkDnsConstraint' instead.+-}+mkDnsName :: Text -> Either DNSNameError DnsName+mkDnsName t+ | T.null t = Left NameEmpty+ | T.length t > 253 = Left NameTooLong+ | otherwise = validateLabels (T.splitOn "." t) >> Right (DnsName t)+ where+ validateLabels [] = Left NameEmpty+ validateLabels (l : ls) = validateFirst l >> mapM_ validateLabel ls++ validateFirst "*" = Right ()+ validateFirst l = validateLabel l++ validateLabel l+ | T.null l = Left NameEmpty+ | T.length l > 63 = Left LabelTooLong+ | otherwise =+ case (T.uncons l, T.unsnoc l) of+ (Just ('-', _), _) -> Left LabelLeadingHyphen+ (_, Just (_, '-')) -> Left LabelTrailingHyphen+ _+ | T.all isValidChar l -> Right ()+ | otherwise -> Left LabelInvalidChar++ isValidChar c = isAlphaNum c || c == '-'+++{- | Construct a 'DnsName' for use as an RFC 5280 name-constraint subtree.++Accepts an optional leading dot (e.g. @\".example.com\"@), which denotes the+domain and all its subdomains. The remainder after stripping the leading dot+must satisfy the same RFC 1123 rules as 'mkDnsName'.++Use 'mkDnsName' for ordinary hostname values; use this function only when+constructing a 'NameConstraints' subtree entry.+-}+mkDnsConstraint :: Text -> Either DNSNameError DnsName+mkDnsConstraint t = case T.stripPrefix "." t of+ Just rest -> mkDnsName rest >> Right (DnsName t)+ Nothing -> mkDnsName t+++-- | Failure modes for 'mkOtherName'.+data OtherNameError+ = -- | The OID arcs are invalid; see 'OIDError'.+ InvalidOID+ | -- | The value contains a code point above U+007F.+ IA5NonAscii+ | -- | The value contains a character outside the PrintableString alphabet.+ PrintableInvalidChar+ | -- | The value contains a code point above U+FFFF.+ BMPNonBMP+ deriving (Eq, Show)+++{- | Construct an 'OtherName', validating the OID and the text value against+the declared 'Asn1StringType' character set.++Returns @Left@ with an 'OtherNameError' if validation fails. The OID is+validated via 'mkOID'. Character-set constraints:++* 'UTF8String' — any 'Text' is accepted.+* 'IA5String' — all code points must be ≤ U+007F.+* 'PrintableString' — all characters must be in @[A-Za-z0-9 \'()+,-./:=?]@.+* 'BMPString' — all code points must be ≤ U+FFFF.+-}+mkOtherName :: Int -> [Int] -> Asn1StringType -> Text -> Either OtherNameError OtherName+mkOtherName firstArc restArcs enc val = do+ oid <- first (const InvalidOID) (mkOID firstArc restArcs)+ validateEncoding enc val+ return (MkOtherName oid enc val)+ where+ validateEncoding UTF8String _ = Right ()+ validateEncoding IA5String t+ | T.all (\c -> fromEnum c <= 127) t = Right ()+ | otherwise = Left IA5NonAscii+ validateEncoding PrintableString t+ | T.all isPrintableChar t = Right ()+ | otherwise = Left PrintableInvalidChar+ validateEncoding BMPString t+ | T.all (\c -> fromEnum c <= 0xFFFF) t = Right ()+ | otherwise = Left BMPNonBMP++ isPrintableChar c = (isAscii c && isAlphaNum c) || c `elem` (" '()+,-./:=?" :: String)+++{- | Construct a 'GeneralName' carrying a validated ASN.1 registered object+identifier.++The OID arcs are validated via 'mkOID'. Returns @Left 'OIDError'@ if+validation fails.+-}+mkRegisteredID :: Int -> [Int] -> Either OIDError GeneralName+mkRegisteredID firstArc restArcs = RegisteredID_ <$> mkOID firstArc restArcs+++{- | Construct a @'Other' 'OtherName'@ 'GeneralName', validating the OID and+encoding in the same way as 'mkOtherName'.+-}+mkOther :: Int -> [Int] -> Asn1StringType -> Text -> Either OtherNameError GeneralName+mkOther firstArc restArcs enc val = Other <$> mkOtherName firstArc restArcs enc val
+ src/Data/Rfc5280/HasOID.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.HasOID+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'HasOID' typeclass for associating extension types with their+RFC 5280 OIDs, and the 'Extension' wrapper for pairing a value with its+criticality flag.+-}+module Data.Rfc5280.HasOID+ ( HasOID (..)+ , Extension (..)+ )+where++import Data.Proxy (Proxy)+import Data.Rfc5280.Internal (OID, RenderConfig (..))+++-- | Associates an extension type with its RFC 5280 OID.+class HasOID a where+ -- | Return the OID assigned to extension type @a@ by RFC 5280.+ extensionOID :: Proxy a -> OID+++{- | Pairs an extension value with its RFC 5280 criticality flag.++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.9 RFC 5280 §4.1.2.9>.+-}+data Extension a = Extension+ { extCritical :: !Bool+ -- ^ 'True' if the extension is marked critical.+ , extValue :: !a+ -- ^ The extension value.+ }+ deriving (Eq, Show)+++{- | Renders as @\"critical,\<value\>\"@ when 'extCritical' is 'True',+or just @\<value\>@ otherwise.+-}+instance (RenderConfig a) => RenderConfig (Extension a) where+ renderBuilder (Extension critical val)+ | critical = "critical," <> renderBuilder val+ | otherwise = renderBuilder val
+ src/Data/Rfc5280/InhibitAnyPolicy.hs view
@@ -0,0 +1,54 @@+{- |+Module : Data.Rfc5280.InhibitAnyPolicy+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'InhibitAnyPolicy', representing the X.509 Inhibit anyPolicy+extension (RFC 5280 §4.2.1.14).+-}+module Data.Rfc5280.InhibitAnyPolicy+ ( InhibitAnyPolicy (..)+ , InhibitAnyPolicyError (..)+ , mkInhibitAnyPolicy+ )+where++import Data.ByteString.Builder (intDec)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..))+++{- | Represents the InhibitAnyPolicy extension (RFC 5280 §4.2.1.14).++The value is the @SkipCerts@ integer, indicating how many additional+certificates may appear in the path before anyPolicy is no longer+acceptable. Use 'mkInhibitAnyPolicy' to construct a value.+-}+newtype InhibitAnyPolicy = InhibitAnyPolicy Int+ deriving (Eq, Show)+++-- | Failure modes for 'mkInhibitAnyPolicy'.+data InhibitAnyPolicyError+ = -- | The skip-certs value is negative.+ NegativeSkipCerts+ deriving (Eq, Show)+++{- | Construct an 'InhibitAnyPolicy', validating that the skip-certs value+is non-negative.+-}+mkInhibitAnyPolicy :: Int -> Either InhibitAnyPolicyError InhibitAnyPolicy+mkInhibitAnyPolicy n+ | n < 0 = Left NegativeSkipCerts+ | otherwise = Right (InhibitAnyPolicy n)+++instance HasOID InhibitAnyPolicy where+ extensionOID _ = 2 :| [5, 29, 54]+++instance RenderConfig InhibitAnyPolicy where+ renderBuilder (InhibitAnyPolicy n) = intDec n
+ src/Data/Rfc5280/Internal.hs view
@@ -0,0 +1,79 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.Internal+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Internal helpers shared across X.509 extension modules. Not part of the+public API stability guarantee.+-}+module Data.Rfc5280.Internal+ ( -- * Rendering+ RenderConfig (..)++ -- * OID+ , OID+ , OIDError (..)+ , mkOID+ , oidBuilder++ -- * Builder helpers+ , intersperseCommas+ , intersperseWith+ )+where++import Data.ByteString.Builder (Builder, intDec)+import Data.Foldable (foldl')+import Data.List.NonEmpty (NonEmpty (..))+++{- | Types that can be rendered to a configuration value as a 'Builder'.+Implemented by all extension types; used by 'Data.Rfc5280.renderConfig'.+-}+class RenderConfig a where+ -- | Render a value as a 'Builder' in OpenSSL extension configuration format.+ renderBuilder :: a -> Builder+++-- | An ASN.1 object identifier. Construct with 'mkOID'.+type OID = NonEmpty Int+++-- | Failure modes for 'mkOID'.+data OIDError+ = -- | First arc is not 0, 1, or 2.+ InvalidFirstArc+ | -- | One or more subsequent arcs are negative.+ NegativeArc+ deriving (Eq, Show)+++{- | Construct an 'OID', validating that the first arc is 0–2 and all arcs+are non-negative.++The second-arc ≤ 39 constraint from X.660 is not enforced; it only applies+under first arcs 0 and 1 and is rarely violated in practice.+-}+mkOID :: Int -> [Int] -> Either OIDError OID+mkOID first rest+ | first < 0 || first > 2 = Left InvalidFirstArc+ | any (< 0) rest = Left NegativeArc+ | otherwise = Right (first :| rest)+++-- | Render an 'OID' as a dot-separated sequence of integers.+oidBuilder :: OID -> Builder+oidBuilder = intersperseWith "." . fmap intDec+++-- | Render a non-empty list of 'Builder' values separated by commas.+intersperseCommas :: NonEmpty Builder -> Builder+intersperseCommas = intersperseWith ","+++-- | Render a non-empty list of 'Builder' values separated by @sep@.+intersperseWith :: Builder -> NonEmpty Builder -> Builder+intersperseWith sep (x :| xs) = x <> foldl' (\acc y -> acc <> sep <> y) "" xs
+ src/Data/Rfc5280/IssuerAltName.hs view
@@ -0,0 +1,41 @@+{- |+Module : Data.Rfc5280.IssuerAltName+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'IssuerAltName', representing the X.509 Issuer Alternative Name+extension (RFC 5280 §4.2.1.7).+-}+module Data.Rfc5280.IssuerAltName+ ( IssuerAltName (..)+ , mkIssuerAltName+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (GeneralName)+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | Represents the IssuerAltName extension (RFC 5280 §4.2.1.7).++Contains one or more 'GeneralName' values identifying the issuer. Use+'mkIssuerAltName' to construct a value.+-}+newtype IssuerAltName = IssuerAltName (NonEmpty GeneralName)+ deriving (Eq, Show)+++-- | Construct an 'IssuerAltName' from one or more 'GeneralName' values.+mkIssuerAltName :: GeneralName -> [GeneralName] -> IssuerAltName+mkIssuerAltName x xs = IssuerAltName (x :| xs)+++instance HasOID IssuerAltName where+ extensionOID _ = 2 :| [5, 29, 18]+++instance RenderConfig IssuerAltName where+ renderBuilder (IssuerAltName names) = intersperseCommas (fmap renderBuilder names)
+ src/Data/Rfc5280/KeyUsage.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.KeyUsage+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'KeyUsage' extension type.+-}+module Data.Rfc5280.KeyUsage+ ( KeyUsageBit (..)+ , KeyUsage+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+import qualified Data.Set.NonEmpty as NES+++{- | Represents the bits that can set for @KeyUsage@++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 RFC 5280 §4.2.1.3>.+-}+data KeyUsageBit+ = {- | Verifying digital signatures other than signatures on certificates or+ CRLs. Renders as @\"digitalSignature\"@.+ -}+ DigitalSignature+ | {- | Verifying digital signatures to provide non-repudiation of signing+ actions (also called @contentCommitment@). Renders as @\"nonRepudiation\"@.+ -}+ NonRepudiation+ | {- | Enciphering private or secret keys (key transport). Renders as+ @\"keyEncipherment\"@.+ -}+ KeyEncipherment+ | {- | Directly enciphering raw user data without an intermediate symmetric+ cipher. Renders as @\"dataEncipherment\"@.+ -}+ DataEncipherment+ | {- | Key agreement protocols (e.g. Diffie-Hellman). Renders as+ @\"keyAgreement\"@.+ -}+ KeyAgreement+ | {- | Verifying signatures on public-key certificates. Renders as+ @\"keyCertSign\"@.+ -}+ KeyCertSign+ | {- | Verifying signatures on certificate revocation lists. Renders as+ @\"cRLSign\"@.+ -}+ CRLSign+ | {- | Enciphering data only during key agreement (used with 'KeyAgreement').+ Renders as @\"encipherOnly\"@.+ -}+ EncipherOnly+ | {- | Deciphering data only during key agreement (used with 'KeyAgreement').+ Renders as @\"decipherOnly\"@.+ -}+ DecipherOnly+ deriving (Eq, Show, Ord, Enum, Bounded)+++instance RenderConfig KeyUsageBit where+ renderBuilder DigitalSignature = "digitalSignature"+ renderBuilder NonRepudiation = "nonRepudiation"+ renderBuilder KeyEncipherment = "keyEncipherment"+ renderBuilder DataEncipherment = "dataEncipherment"+ renderBuilder KeyAgreement = "keyAgreement"+ renderBuilder KeyCertSign = "keyCertSign"+ renderBuilder CRLSign = "cRLSign"+ renderBuilder EncipherOnly = "encipherOnly"+ renderBuilder DecipherOnly = "decipherOnly"+++{- | Represents the 'KeyUsage' extension++See <https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 RFC 5280 §4.2.1.3>.+-}+type KeyUsage = NES.NESet KeyUsageBit+++instance HasOID KeyUsage where+ extensionOID _ = 2 :| [5, 29, 15]+++instance RenderConfig KeyUsage where+ renderBuilder = intersperseCommas . fmap renderBuilder . NES.toList
+ src/Data/Rfc5280/NameConstraints.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.NameConstraints+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'NameConstraints', representing the X.509 Name Constraints extension+(RFC 5280 §4.2.1.10).++The @minimum@ and @maximum@ fields of @GeneralSubtree@ are not modelled; they+are almost never used in practice and are not supported in OpenSSL config format.+-}+module Data.Rfc5280.NameConstraints+ ( NameConstraints (..)+ , mkNameConstraints+ , NameConstraint (..)+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (GeneralName)+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | A single name constraint, either permitting or excluding a subtree+identified by a 'GeneralName'.+-}+data NameConstraint+ = -- | A permitted subtree. Rendered as @permitted;\<name\>@.+ Permitted !GeneralName+ | -- | An excluded subtree. Rendered as @excluded;\<name\>@.+ Excluded !GeneralName+ deriving (Eq, Show)+++{- | Represents the NameConstraints extension (RFC 5280 §4.2.1.10).++Contains one or more 'NameConstraint' values. Use 'mkNameConstraints' to+construct a value.+-}+newtype NameConstraints = NameConstraints (NonEmpty NameConstraint)+ deriving (Eq, Show)+++-- | Construct a 'NameConstraints' from one or more 'NameConstraint' values.+mkNameConstraints :: NameConstraint -> [NameConstraint] -> NameConstraints+mkNameConstraints x xs = NameConstraints (x :| xs)+++instance HasOID NameConstraints where+ extensionOID _ = 2 :| [5, 29, 30]+++instance RenderConfig NameConstraint where+ renderBuilder (Permitted name) = "permitted;" <> renderBuilder name+ renderBuilder (Excluded name) = "excluded;" <> renderBuilder name+++instance RenderConfig NameConstraints where+ renderBuilder (NameConstraints cs) = intersperseCommas (fmap renderBuilder cs)
+ src/Data/Rfc5280/PolicyMappings.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.PolicyMappings+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'PolicyMappings', representing the X.509 Policy Mappings extension+(RFC 5280 §4.2.1.5).+-}+module Data.Rfc5280.PolicyMappings+ ( PolicyMappings (..)+ , mkPolicyMappings+ , PolicyMapping (..)+ , mkPolicyMapping+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (OID, RenderConfig (..), intersperseCommas, oidBuilder)+++{- | A single policy mapping, pairing an issuer domain policy OID with a+subject domain policy OID.+-}+data PolicyMapping = PolicyMapping+ { pmIssuerDomainPolicy :: !OID+ -- ^ The issuer's policy OID.+ , pmSubjectDomainPolicy :: !OID+ -- ^ The subject's corresponding policy OID.+ }+ deriving (Eq, Show)+++{- | Represents the PolicyMappings extension (RFC 5280 §4.2.1.5).++Contains one or more 'PolicyMapping' pairs. Use 'mkPolicyMappings' to+construct a value.+-}+newtype PolicyMappings = PolicyMappings (NonEmpty PolicyMapping)+ deriving (Eq, Show)+++-- | Construct a 'PolicyMapping' from an issuer domain policy OID and a subject domain policy OID.+mkPolicyMapping :: OID -> OID -> PolicyMapping+mkPolicyMapping = PolicyMapping+++-- | Construct a 'PolicyMappings' from one or more 'PolicyMapping' values.+mkPolicyMappings :: PolicyMapping -> [PolicyMapping] -> PolicyMappings+mkPolicyMappings x xs = PolicyMappings (x :| xs)+++instance HasOID PolicyMappings where+ extensionOID _ = 2 :| [5, 29, 33]+++instance RenderConfig PolicyMapping where+ renderBuilder pm = oidBuilder (pmIssuerDomainPolicy pm) <> ":" <> oidBuilder (pmSubjectDomainPolicy pm)+++instance RenderConfig PolicyMappings where+ renderBuilder (PolicyMappings ms) = intersperseCommas (fmap renderBuilder ms)
+ src/Data/Rfc5280/SubjectAltName.hs view
@@ -0,0 +1,41 @@+{- |+Module : Data.Rfc5280.SubjectAltName+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides 'SubjectAltName', representing the X.509 Subject Alternative Name+extension (RFC 5280 §4.2.1.6).+-}+module Data.Rfc5280.SubjectAltName+ ( SubjectAltName (..)+ , mkSubjectAltName+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (GeneralName)+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..), intersperseCommas)+++{- | Represents the SubjectAltName extension (RFC 5280 §4.2.1.6).++Contains one or more 'GeneralName' values identifying the subject. Use+'mkSubjectAltName' to construct a value.+-}+newtype SubjectAltName = SubjectAltName (NonEmpty GeneralName)+ deriving (Eq, Show)+++-- | Construct a 'SubjectAltName' from one or more 'GeneralName' values.+mkSubjectAltName :: GeneralName -> [GeneralName] -> SubjectAltName+mkSubjectAltName x xs = SubjectAltName (x :| xs)+++instance HasOID SubjectAltName where+ extensionOID _ = 2 :| [5, 29, 17]+++instance RenderConfig SubjectAltName where+ renderBuilder (SubjectAltName names) = intersperseCommas (fmap renderBuilder names)
+ src/Data/Rfc5280/SubjectKeyIdentifier.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Data.Rfc5280.SubjectKeyIdentifier+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Provides the 'SubjectKeyIdentifier' extension type.+-}+module Data.Rfc5280.SubjectKeyIdentifier+ ( SubjectKeyIdentifier (..)+ )+where++import Data.ByteString (ByteString)+import Data.ByteString.Builder (byteString)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.HasOID (HasOID (..))+import Data.Rfc5280.Internal (RenderConfig (..))+++-- | Represents a SubjectKeyIdentifier value (RFC 5280 §4.2.1.2).+data SubjectKeyIdentifier+ = {- | A raw key identifier. The caller supplies the bytes directly —+ typically the SHA-1 hash of the BIT STRING value of the+ subjectPublicKey field.+ -}+ Raw !ByteString+ | {- | Use the default hash method: the 160-bit SHA-1 hash of the+ subjectPublicKey BIT STRING, as defined in RFC 5280 §4.2.1.2.+ -}+ HashMethod+ deriving (Eq, Show)+++instance HasOID SubjectKeyIdentifier where+ extensionOID _ = 2 :| [5, 29, 14]+++instance RenderConfig SubjectKeyIdentifier where+ renderBuilder (Raw x) = byteString x+ renderBuilder HashMethod = "hash"
+ test/Rfc5280/AssertSpec.hs view
@@ -0,0 +1,30 @@+{- |+Module : Rfc5280.AssertSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.Assert'.+-}+module Rfc5280.AssertSpec (spec) where++import Data.Rfc5280.Assert (assertJust, assertRight)+import Test.Hspec+++spec :: Spec+spec = do+ describe "assertRight" $ do+ it "returns the value on Right" $ do+ result <- assertRight (Right 42 :: Either String Int)+ result `shouldBe` 42+ it "fails on Left" $+ assertRight (Left "oops" :: Either String Int)+ `shouldThrow` anyException+ describe "assertJust" $ do+ it "returns the value on Just" $ do+ result <- assertJust "missing" (Just 42)+ result `shouldBe` (42 :: Int)+ it "fails with the given message on Nothing" $+ assertJust "missing" (Nothing :: Maybe Int)+ `shouldThrow` anyException
+ test/Rfc5280/AuthorityInfoAccessSpec.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.AuthorityInfoAccessSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.AuthorityInfoAccess'.+-}+module Rfc5280.AuthorityInfoAccessSpec (spec) where++import qualified Data.ByteString as BS+import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.AuthorityInfoAccess+import Data.Rfc5280.GeneralName+import Rfc5280.Generators (validDnsName)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (forAll)+import Text.URI (mkURI)+++spec :: Spec+spec = describe "module Data.Rfc5280.AuthorityInfoAccess" $ do+ context "mkAuthorityInfoAccess" $ do+ it "renders a single OCSP entry" $ do+ uri <- mkURI "http://ocsp.example.com"+ renderConfig (mkAuthorityInfoAccess (OCSP (URIName uri)) [])+ `shouldBe` "OCSP;URI:http://ocsp.example.com"+ it "renders a single CAIssuers entry" $ do+ uri <- mkURI "http://ca.example.com/issuer.crt"+ renderConfig (mkAuthorityInfoAccess (CAIssuers (URIName uri)) [])+ `shouldBe` "caIssuers;URI:http://ca.example.com/issuer.crt"+ it "renders OCSP and CAIssuers together" $ do+ ocspUri <- mkURI "http://ocsp.example.com"+ issuerUri <- mkURI "http://ca.example.com/issuer.crt"+ renderConfig+ ( mkAuthorityInfoAccess+ (OCSP (URIName ocspUri))+ [CAIssuers (URIName issuerUri)]+ )+ `shouldBe` "OCSP;URI:http://ocsp.example.com,caIssuers;URI:http://ca.example.com/issuer.crt"+ it "renders an Other name as OCSP location" $ do+ gn <- assertRight (mkOther 1 [2, 3] UTF8String "value")+ renderConfig (mkAuthorityInfoAccess (OCSP gn) [])+ `shouldBe` "OCSP;otherName:1.2.3;UTF8:value"+ prop "OCSP entries always start with OCSP;" $+ forAll validDnsName $ \dn ->+ BS.isPrefixOf "OCSP;" (renderConfig (mkAuthorityInfoAccess (OCSP (DNS dn)) []))+ prop "CAIssuers entries always start with caIssuers;" $+ forAll validDnsName $ \dn ->+ BS.isPrefixOf "caIssuers;" (renderConfig (mkAuthorityInfoAccess (CAIssuers (DNS dn)) []))
+ test/Rfc5280/CRLDistributionPointsSpec.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.CRLDistributionPointsSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.CRLDistributionPoints'.+-}+module Rfc5280.CRLDistributionPointsSpec (spec) where++import qualified Data.ByteString as BS+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.CRLDistributionPoints+import Data.Rfc5280.GeneralName+import Rfc5280.Generators (validDnsName, vectorOf1)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (choose, forAll, (===))+import Text.URI (mkURI)+++spec :: Spec+spec = describe "module Data.Rfc5280.CRLDistributionPoints" $ do+ context "mkCRLDistributionPoints" $ do+ it "renders a single URI distribution point" $ do+ uri <- mkURI "http://crl.example.com/crl.crl"+ renderConfig (mkCRLDistributionPoints (mkDistributionPoint (URIName uri)) [])+ `shouldBe` "URI:http://crl.example.com/crl.crl"+ it "renders two URI distribution points" $ do+ uri1 <- mkURI "http://crl1.example.com/crl.crl"+ uri2 <- mkURI "http://crl2.example.com/crl.crl"+ renderConfig+ ( mkCRLDistributionPoints+ (mkDistributionPoint (URIName uri1))+ [mkDistributionPoint (URIName uri2)]+ )+ `shouldBe` "URI:http://crl1.example.com/crl.crl,URI:http://crl2.example.com/crl.crl"+ it "renders a DNS-name distribution point" $ do+ dn <- assertRight (mkDnsName "crl.example.com")+ renderConfig (mkCRLDistributionPoints (mkDistributionPoint (DNS dn)) [])+ `shouldBe` "DNS:crl.example.com"+ it "renders an Other distribution point" $ do+ gn <- assertRight (mkOther 1 [2, 3] UTF8String "value")+ renderConfig+ ( mkCRLDistributionPoints+ (mkDistributionPoint gn)+ []+ )+ `shouldBe` "otherName:1.2.3;UTF8:value"+ prop "a single distribution point contains no comma" $+ forAll validDnsName $ \dn ->+ BS.elem 0x2C (renderConfig (mkCRLDistributionPoints (mkDistributionPoint (DNS dn)) [])) === False+ prop "n distribution points produce exactly n-1 comma separators" $+ forAll (choose (1, 6)) $ \n ->+ forAll (vectorOf1 n validDnsName) $ \(h :| tl) ->+ let pts =+ mkCRLDistributionPoints+ (mkDistributionPoint (DNS h))+ (map (mkDistributionPoint . DNS) tl)+ bs = renderConfig pts+ in BS.length (BS.filter (== 0x2C) bs) === n - 1
+ test/Rfc5280/Fixtures.hs view
@@ -0,0 +1,38 @@+{- |+Module : Rfc5280.Fixtures+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Shared test helpers for X.509 extension test suites.++'testIP' and 'testEmail' lift external-library decoders that use 'Maybe' and+@Either String@ respectively into 'MonadIO', throwing a 'userError' when+decoding fails.+-}+module Rfc5280.Fixtures+ ( testIP+ , testEmail+ )+where++import Control.Exception (throwIO)+import Control.Monad.IO.Class (MonadIO, liftIO)+import Data.ByteString (ByteString)+import Data.Rfc5280.Assert (assertJust)+import Data.Text (Text)+import qualified Data.Text as T+import Net.IP (IP)+import qualified Net.IP as IP+import Text.Email.Validate (EmailAddress, validate)+++-- | Decode an IP address, throwing a 'userError' with a descriptive message if invalid.+testIP :: (MonadIO m) => Text -> m IP+testIP t = assertJust ("could not decode IP address: " <> T.unpack t) (IP.decode t)+++-- | Parse an email address, throwing a 'userError' with a descriptive message if invalid.+testEmail :: (MonadIO m) => ByteString -> m EmailAddress+testEmail bs =+ either (\e -> liftIO . throwIO . userError $ "invalid email address: " <> e) pure (validate bs)
+ test/Rfc5280/GeneralNameSpec.hs view
@@ -0,0 +1,108 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.GeneralNameSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.GeneralName'.+-}+module Rfc5280.GeneralNameSpec (spec) where++import Data.Either (isLeft)+import Data.Rfc5280 (NonEmpty (..), renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.GeneralName+import qualified Data.Text as T+import Rfc5280.Fixtures (testEmail, testIP)+import Rfc5280.Generators (nameWithInvalidChar, validDNSName)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (forAll, (===))+import Text.URI (mkURI)+++spec :: Spec+spec = describe "module Data.Rfc5280.GeneralName" $ do+ context "DNS" $+ it "converts to ByteString" $ do+ dn <- assertRight (mkDnsName "example.com")+ renderConfig (DNS dn) `shouldBe` "DNS:example.com"+ context "IPAddr (IPv4)" $+ it "converts to ByteString" $ do+ ip <- testIP "192.0.2.1"+ renderConfig (IPAddr ip) `shouldBe` "IP:192.0.2.1"+ context "IPAddr (IPv6)" $+ it "converts to ByteString" $ do+ ip <- testIP "::1"+ renderConfig (IPAddr ip) `shouldBe` "IP:::1"+ context "EmailAddr" $+ it "converts to ByteString" $ do+ addr <- testEmail "user@example.com"+ renderConfig (EmailAddr addr) `shouldBe` "email:user@example.com"+ context "URIName" $+ it "converts to ByteString" $ do+ uri <- mkURI "https://example.com"+ renderConfig (URIName uri) `shouldBe` "URI:https://example.com"+ context "RegisteredID" $+ it "converts to ByteString" $ do+ rid <- assertRight (mkRegisteredID 2 [5, 4, 3])+ renderConfig rid `shouldBe` "RID:2.5.4.3"+ context "Other (UTF8String)" $+ it "converts to ByteString" $ do+ gn <- assertRight (mkOther 1 [2, 3] UTF8String "hello")+ renderConfig gn `shouldBe` "otherName:1.2.3;UTF8:hello"+ context "Other (IA5String)" $+ it "converts to ByteString" $ do+ gn <- assertRight (mkOther 1 [2, 3] IA5String "hello")+ renderConfig gn `shouldBe` "otherName:1.2.3;IA5:hello"+ context "Other (PrintableString)" $+ it "converts to ByteString" $ do+ gn <- assertRight (mkOther 1 [2, 3] PrintableString "Hello World")+ renderConfig gn `shouldBe` "otherName:1.2.3;PRINTABLE:Hello World"+ context "Other (BMPString)" $+ it "converts to ByteString" $ do+ gn <- assertRight (mkOther 1 [2, 3] BMPString "hello")+ renderConfig gn `shouldBe` "otherName:1.2.3;BMP:hello"+ context "mkDnsName" $ do+ it "accepts a simple hostname" $+ fmap dnsNameText (mkDnsName "example.com") `shouldBe` Right "example.com"+ it "accepts a multi-label hostname" $+ fmap dnsNameText (mkDnsName "foo.bar.example.com") `shouldBe` Right "foo.bar.example.com"+ it "accepts a wildcard first label" $+ fmap dnsNameText (mkDnsName "*.example.com") `shouldBe` Right "*.example.com"+ it "accepts a single label" $+ fmap dnsNameText (mkDnsName "localhost") `shouldBe` Right "localhost"+ it "rejects an empty name" $+ mkDnsName "" `shouldBe` Left NameEmpty+ it "rejects a label ending with a hyphen" $+ mkDnsName "example-.com" `shouldBe` Left LabelTrailingHyphen+ it "rejects a label starting with a hyphen" $+ mkDnsName "-example.com" `shouldBe` Left LabelLeadingHyphen+ it "rejects an empty label" $+ mkDnsName "example..com" `shouldBe` Left NameEmpty+ it "rejects a label exceeding 63 characters" $+ mkDnsName (T.replicate 64 "a" <> ".com") `shouldBe` Left LabelTooLong+ it "rejects a name exceeding 253 characters" $+ mkDnsName (T.intercalate "." (replicate 5 (T.replicate 50 "a"))) `shouldBe` Left NameTooLong+ prop "accepts any validly-constructed hostname" $+ forAll validDNSName $+ \t -> fmap dnsNameText (mkDnsName t) === Right t+ prop "rejects any name containing an invalid label character" $+ forAll nameWithInvalidChar $+ \t -> isLeft (mkDnsName t)+ context "mkOtherName" $ do+ it "accepts a valid OID and UTF8String value" $ do+ on <- assertRight (mkOtherName 1 [2, 3] UTF8String "hello")+ onTypeId on `shouldBe` (1 :| [2, 3])+ onEncoding on `shouldBe` UTF8String+ onValue on `shouldBe` "hello"+ it "rejects a negative OID arc" $+ mkOtherName (-1) [2, 3] UTF8String "hello" `shouldBe` Left InvalidOID+ it "rejects IA5String value containing a non-ASCII character" $+ mkOtherName 1 [2, 3] IA5String "h\xe9llo" `shouldBe` Left IA5NonAscii+ it "rejects PrintableString value containing a character outside the alphabet" $+ mkOtherName 1 [2, 3] PrintableString "user@example" `shouldBe` Left PrintableInvalidChar+ it "rejects BMPString value containing a non-BMP character" $+ mkOtherName 1 [2, 3] BMPString "\x1F600" `shouldBe` Left BMPNonBMP
+ test/Rfc5280/Generators.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.Generators+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Shared QuickCheck generators for X.509 extension test suites.+-}+module Rfc5280.Generators+ ( validLabel+ , validDNSName+ , validDnsName+ , nameWithInvalidChar+ , vectorOf1+ )+where++import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280.GeneralName (DnsName, mkDnsName)+import qualified Data.Text as T+import Test.QuickCheck (Gen, choose, elements, vectorOf)+++-- | Generates a DNS label of 1–10 lowercase alphanumeric characters.+validLabel :: Gen T.Text+validLabel = do+ n <- choose (1, 10)+ T.pack <$> vectorOf n (elements (['a' .. 'z'] ++ ['0' .. '9']))+++-- | Generates a valid DNS name of 1–4 alphanumeric-only labels.+validDNSName :: Gen T.Text+validDNSName = do+ n <- choose (1, 4)+ labels <- vectorOf n validLabel+ return $ T.intercalate "." labels+++{- | Generates a valid 'DnsName'.++Wraps 'validDNSName' and applies 'mkDnsName'; panics if the generator+produces an invalid name (which it never should).+-}+validDnsName :: Gen DnsName+validDnsName = do+ t <- validDNSName+ case mkDnsName t of+ Right dn -> return dn+ Left err -> error $ "validDNSName produced invalid DNS name: " <> show err+++{- | Like 'vectorOf' but returns a 'NonEmpty' list, guaranteeing at least one+element. The count @n@ must be ≥ 1; callers should enforce this with+'choose' or similar.+-}+vectorOf1 :: Int -> Gen a -> Gen (NonEmpty a)+vectorOf1 n gen = do+ h <- gen+ tl <- vectorOf (n - 1) gen+ return (h :| tl)+++-- | Generates a single label with one invalid character injected in the middle.+nameWithInvalidChar :: Gen T.Text+nameWithInvalidChar = do+ prefix <- validLabel+ badChar <- elements "!@#$%^&*()"+ suffix <- validLabel+ return $ prefix <> T.singleton badChar <> suffix
+ test/Rfc5280/HasOIDSpec.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.HasOIDSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.HasOID'.+-}+module Rfc5280.HasOIDSpec (spec) where++import Data.Proxy (Proxy (..))+import Data.Rfc5280+ ( AuthorityKeyIdentifier+ , BasicConstraints (..)+ , CertificatePolicies+ , ExtKeyUsage+ , KeyUsage+ , NonEmpty (..)+ , SubjectKeyIdentifier+ , renderConfig+ )+import Data.Rfc5280.AuthorityInfoAccess (AuthorityInfoAccess)+import Data.Rfc5280.HasOID (Extension (..), HasOID (..), extensionOID)+import Data.Rfc5280.SubjectAltName (SubjectAltName)+import Test.Hspec+++spec :: Spec+spec = describe "module Data.Rfc5280.HasOID" $ do+ context "extensionOID" $ do+ it "returns the OID for BasicConstraints" $+ extensionOID (Proxy :: Proxy BasicConstraints) `shouldBe` (2 :| [5, 29, 19])+ it "returns the OID for KeyUsage" $+ extensionOID (Proxy :: Proxy KeyUsage) `shouldBe` (2 :| [5, 29, 15])+ it "returns the OID for ExtKeyUsage" $+ extensionOID (Proxy :: Proxy ExtKeyUsage) `shouldBe` (2 :| [5, 29, 37])+ it "returns the OID for SubjectKeyIdentifier" $+ extensionOID (Proxy :: Proxy SubjectKeyIdentifier) `shouldBe` (2 :| [5, 29, 14])+ it "returns the OID for AuthorityKeyIdentifier" $+ extensionOID (Proxy :: Proxy AuthorityKeyIdentifier) `shouldBe` (2 :| [5, 29, 35])+ it "returns the OID for CertificatePolicies" $+ extensionOID (Proxy :: Proxy CertificatePolicies) `shouldBe` (2 :| [5, 29, 32])+ it "returns the OID for SubjectAltName" $+ extensionOID (Proxy :: Proxy SubjectAltName) `shouldBe` (2 :| [5, 29, 17])+ it "returns the OID for AuthorityInfoAccess" $+ extensionOID (Proxy :: Proxy AuthorityInfoAccess) `shouldBe` (1 :| [3, 6, 1, 5, 5, 7, 1, 1])+ context "Extension (RenderConfig)" $ do+ it "prepends 'critical,' when extCritical is True" $+ renderConfig (Extension True notCA) `shouldBe` "critical,CA:FALSE"+ it "renders the value unchanged when extCritical is False" $+ renderConfig (Extension False notCA) `shouldBe` "CA:FALSE"+++notCA :: BasicConstraints+notCA = BasicConstraints{bcIsCA = False, bcPathLength = Nothing}
+ test/Rfc5280/InhibitAnyPolicySpec.hs view
@@ -0,0 +1,31 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.InhibitAnyPolicySpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.InhibitAnyPolicy'.+-}+module Rfc5280.InhibitAnyPolicySpec (spec) where++import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.InhibitAnyPolicy+import Test.Hspec+++spec :: Spec+spec = describe "module Data.Rfc5280.InhibitAnyPolicy" $ do+ context "mkInhibitAnyPolicy" $ do+ it "accepts zero" $+ mkInhibitAnyPolicy 0 `shouldBe` Right (InhibitAnyPolicy 0)+ it "accepts a positive value" $+ mkInhibitAnyPolicy 2 `shouldBe` Right (InhibitAnyPolicy 2)+ it "rejects a negative value" $+ mkInhibitAnyPolicy (-1) `shouldBe` Left NegativeSkipCerts+ context "RenderConfig" $ do+ it "renders zero" $+ renderConfig (InhibitAnyPolicy 0) `shouldBe` "0"+ it "renders a positive value" $+ renderConfig (InhibitAnyPolicy 5) `shouldBe` "5"
+ test/Rfc5280/IssuerAltNameSpec.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.IssuerAltNameSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.IssuerAltName'.+-}+module Rfc5280.IssuerAltNameSpec (spec) where++import qualified Data.ByteString as BS+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.GeneralName+import Data.Rfc5280.IssuerAltName+import Rfc5280.Fixtures (testEmail, testIP)+import Rfc5280.Generators (validDnsName, vectorOf1)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (choose, forAll, (===))+++spec :: Spec+spec = describe "module Data.Rfc5280.IssuerAltName" $ do+ context "mkIssuerAltName" $ do+ it "renders a single DNS name" $ do+ dn <- assertRight (mkDnsName "example.com")+ renderConfig (mkIssuerAltName (DNS dn) [])+ `shouldBe` "DNS:example.com"+ it "renders two DNS names" $ do+ dn1 <- assertRight (mkDnsName "example.com")+ dn2 <- assertRight (mkDnsName "www.example.com")+ renderConfig (mkIssuerAltName (DNS dn1) [DNS dn2])+ `shouldBe` "DNS:example.com,DNS:www.example.com"+ it "renders a wildcard alongside its base domain" $ do+ wild <- assertRight (mkDnsName "*.example.com")+ base <- assertRight (mkDnsName "example.com")+ renderConfig (mkIssuerAltName (DNS wild) [DNS base])+ `shouldBe` "DNS:*.example.com,DNS:example.com"+ it "renders mixed DNS, IP and email names" $ do+ ip <- testIP "192.0.2.1"+ addr <- testEmail "user@example.com"+ dn <- assertRight (mkDnsName "example.com")+ renderConfig+ ( mkIssuerAltName+ (DNS dn)+ [IPAddr ip, EmailAddr addr]+ )+ `shouldBe` "DNS:example.com,IP:192.0.2.1,email:user@example.com"+ it "renders an Other name" $ do+ gn <- assertRight (mkOther 1 [2, 3] UTF8String "value")+ renderConfig (mkIssuerAltName gn [])+ `shouldBe` "otherName:1.2.3;UTF8:value"+ prop "a single-name IAN contains no comma" $+ forAll validDnsName $ \dn ->+ BS.elem 0x2C (renderConfig (mkIssuerAltName (DNS dn) [])) === False+ prop "n DNS names produce exactly n-1 comma separators" $+ forAll (choose (1, 6)) $ \n ->+ forAll (vectorOf1 n validDnsName) $ \(h :| tl) ->+ let bs = renderConfig (mkIssuerAltName (DNS h) (map DNS tl))+ in BS.length (BS.filter (== 0x2C) bs) === n - 1
+ test/Rfc5280/NameConstraintsSpec.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.NameConstraintsSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.NameConstraints'.+-}+module Rfc5280.NameConstraintsSpec (spec) where++import qualified Data.ByteString as BS+import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.GeneralName+import Data.Rfc5280.NameConstraints+import Rfc5280.Generators (validDnsName)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (forAll, (===))+++spec :: Spec+spec = describe "module Data.Rfc5280.NameConstraints" $ do+ context "mkNameConstraints" $ do+ it "renders a single permitted DNS constraint" $ do+ dn <- assertRight (mkDnsConstraint ".example.com")+ renderConfig (mkNameConstraints (Permitted (DNS dn)) [])+ `shouldBe` "permitted;DNS:.example.com"+ it "renders a single excluded DNS constraint" $ do+ dn <- assertRight (mkDnsConstraint ".example.com")+ renderConfig (mkNameConstraints (Excluded (DNS dn)) [])+ `shouldBe` "excluded;DNS:.example.com"+ it "renders a single excluded email constraint" $ do+ dn <- assertRight (mkDnsConstraint ".example.org")+ renderConfig (mkNameConstraints (Excluded (DNS dn)) [])+ `shouldBe` "excluded;DNS:.example.org"+ it "renders permitted and excluded constraints together" $ do+ permitted <- assertRight (mkDnsConstraint ".example.com")+ excluded <- assertRight (mkDnsConstraint ".evil.example.com")+ renderConfig+ ( mkNameConstraints+ (Permitted (DNS permitted))+ [Excluded (DNS excluded)]+ )+ `shouldBe` "permitted;DNS:.example.com,excluded;DNS:.evil.example.com"+ it "renders multiple permitted constraints" $ do+ dn1 <- assertRight (mkDnsConstraint ".example.com")+ dn2 <- assertRight (mkDnsConstraint ".example.org")+ renderConfig+ ( mkNameConstraints+ (Permitted (DNS dn1))+ [Permitted (DNS dn2)]+ )+ `shouldBe` "permitted;DNS:.example.com,permitted;DNS:.example.org"+ prop "a permitted constraint output starts with \"permitted;\"" $+ forAll validDnsName $ \dn ->+ BS.isPrefixOf "permitted;" (renderConfig (mkNameConstraints (Permitted (DNS dn)) [])) === True+ prop "an excluded constraint output starts with \"excluded;\"" $+ forAll validDnsName $ \dn ->+ BS.isPrefixOf "excluded;" (renderConfig (mkNameConstraints (Excluded (DNS dn)) [])) === True
+ test/Rfc5280/PolicyMappingsSpec.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.PolicyMappingsSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.PolicyMappings'.+-}+module Rfc5280.PolicyMappingsSpec (spec) where++import Data.Rfc5280 (NonEmpty (..), renderConfig)+import Data.Rfc5280.PolicyMappings+import Test.Hspec+++spec :: Spec+spec = describe "module Data.Rfc5280.PolicyMappings" $ do+ context "mkPolicyMappings" $ do+ it "renders a single mapping" $+ renderConfig (mkPolicyMappings (mkPolicyMapping (1 :| [2, 3]) (2 :| [5, 4])) [])+ `shouldBe` "1.2.3:2.5.4"+ it "renders two mappings" $+ renderConfig+ ( mkPolicyMappings+ (mkPolicyMapping (1 :| [2, 3]) (2 :| [5, 4]))+ [mkPolicyMapping (1 :| [2, 4]) (2 :| [5, 5])]+ )+ `shouldBe` "1.2.3:2.5.4,1.2.4:2.5.5"+ it "renders a mapping with multi-arc OIDs" $+ renderConfig+ ( mkPolicyMappings+ (mkPolicyMapping (2 :| [16, 840, 1, 101, 3, 2, 1, 3, 6]) (2 :| [16, 840, 1, 101, 3, 2, 1, 12, 4]))+ []+ )+ `shouldBe` "2.16.840.1.101.3.2.1.3.6:2.16.840.1.101.3.2.1.12.4"
+ test/Rfc5280/SubjectAltNameSpec.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280.SubjectAltNameSpec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Tests for 'Data.Rfc5280.SubjectAltName'.+-}+module Rfc5280.SubjectAltNameSpec (spec) where++import qualified Data.ByteString as BS+import Data.List.NonEmpty (NonEmpty (..))+import Data.Rfc5280 (renderConfig)+import Data.Rfc5280.Assert (assertRight)+import Data.Rfc5280.GeneralName+import Data.Rfc5280.SubjectAltName+import Rfc5280.Fixtures (testEmail, testIP)+import Rfc5280.Generators (validDnsName, vectorOf1)+import Test.Hspec+import Test.Hspec.QuickCheck (prop)+import Test.QuickCheck (choose, forAll, (===))+++spec :: Spec+spec = describe "module Data.Rfc5280.SubjectAltName" $ do+ context "mkSubjectAltName" $ do+ it "renders a single DNS name" $ do+ dn <- assertRight (mkDnsName "example.com")+ renderConfig (mkSubjectAltName (DNS dn) [])+ `shouldBe` "DNS:example.com"+ it "renders two DNS names" $ do+ dn1 <- assertRight (mkDnsName "example.com")+ dn2 <- assertRight (mkDnsName "www.example.com")+ renderConfig (mkSubjectAltName (DNS dn1) [DNS dn2])+ `shouldBe` "DNS:example.com,DNS:www.example.com"+ it "renders a wildcard alongside its base domain" $ do+ wild <- assertRight (mkDnsName "*.example.com")+ base <- assertRight (mkDnsName "example.com")+ renderConfig (mkSubjectAltName (DNS wild) [DNS base])+ `shouldBe` "DNS:*.example.com,DNS:example.com"+ it "renders mixed DNS, IP and email names" $ do+ ip <- testIP "192.0.2.1"+ addr <- testEmail "user@example.com"+ dn <- assertRight (mkDnsName "example.com")+ renderConfig+ ( mkSubjectAltName+ (DNS dn)+ [IPAddr ip, EmailAddr addr]+ )+ `shouldBe` "DNS:example.com,IP:192.0.2.1,email:user@example.com"+ it "renders an Other name" $ do+ gn <- assertRight (mkOther 1 [2, 3] UTF8String "value")+ renderConfig (mkSubjectAltName gn [])+ `shouldBe` "otherName:1.2.3;UTF8:value"+ prop "a single-name SAN contains no comma" $+ forAll validDnsName $ \dn ->+ BS.elem 0x2C (renderConfig (mkSubjectAltName (DNS dn) [])) === False+ prop "n DNS names produce exactly n-1 comma separators" $+ forAll (choose (1, 6)) $ \n ->+ forAll (vectorOf1 n validDnsName) $ \(h :| tl) ->+ let bs = renderConfig (mkSubjectAltName (DNS h) (map DNS tl))+ in BS.length (BS.filter (== 0x2C) bs) === n - 1
+ test/Rfc5280Spec.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE OverloadedStrings #-}++{- |+Module : Rfc5280Spec+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3+-}+module Rfc5280Spec (spec) where++import Data.Rfc5280+import Test.Hspec+++spec :: Spec+spec = describe "module Data.Rfc5280" $ do+ context "BasicConstraints" $ do+ it "renders a non-CA certificate" $+ renderConfig notCA `shouldBe` "CA:FALSE"+ it "renders a CA certificate" $+ renderConfig isCA `shouldBe` "CA:TRUE"+ it "renders a CA certificate with path length" $+ renderConfig caWithPathLen `shouldBe` "CA:TRUE,pathLen3"+ context "KeyUsage" $ do+ it "renders multiple bits" $+ renderConfig simpleKeyUsage `shouldBe` "digitalSignature,cRLSign"+ it "renders a single bit without a comma" $+ renderConfig singleKeyUsage `shouldBe` "digitalSignature"+ it "renders all bits in Enum order" $+ renderConfig allKeyUsageBits+ `shouldBe` "digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment,keyAgreement,keyCertSign,cRLSign,encipherOnly,decipherOnly"+ context "ExtKeyUsage" $ do+ it "renders multiple purposes" $+ renderConfig simpleExtKeyUsage `shouldBe` "serverAuth,codeSigning"+ it "renders a single purpose without a comma" $+ renderConfig singleExtKeyUsage `shouldBe` "serverAuth"+ it "renders all purposes in Enum order" $+ renderConfig allExtKeyUsagePurposes+ `shouldBe` "serverAuth,clientAuth,codeSigning,emailProtection,timeStamping,OCSPSigning,anyExtendedKeyUsage"+ context "SubjectKeyIdentifier" $ do+ it "renders the hash method" $+ renderConfig HashMethod `shouldBe` "hash"+ it "renders raw bytes" $+ renderConfig (Raw "abc") `shouldBe` "abc"+ context "AuthorityKeyIdentifier" $ do+ it "renders keyid:always" $+ renderConfig simpleAKI `shouldBe` "keyid:always"+ it "renders keyid" $+ renderConfig keyIdOnly `shouldBe` "keyid"+ it "renders issuer" $+ renderConfig issuerOnly `shouldBe` "issuer"+ it "renders issuer:always" $+ renderConfig issuerAlwaysAKI `shouldBe` "issuer:always"+ it "renders keyid:always and issuer:always combined" $+ renderConfig bothAlways `shouldBe` "keyid:always,issuer:always"+ it "renders empty when all flags are disabled" $+ renderConfig allDisabled `shouldBe` ""+ context "CertificatePolicies" $ do+ it "renders a single OID" $+ renderConfig simpleCP `shouldBe` "1.2.3.4"+ it "renders multiple OIDs" $+ renderConfig twoOIDs `shouldBe` "1.2.3.4,2.5.4.3"+ context "mkBasicConstraints" $ do+ it "accepts a non-CA without path length" $+ mkBasicConstraints False Nothing `shouldBe` Right notCA+ it "accepts a CA without path length" $+ mkBasicConstraints True Nothing `shouldBe` Right isCA+ it "accepts a CA with path length" $+ mkBasicConstraints True (Just 3) `shouldBe` Right caWithPathLen+ it "rejects a non-CA with path length" $+ mkBasicConstraints False (Just 0) `shouldBe` Left PathLenWithoutCA+ context "mkAuthorityKeyIdentifier" $+ it "sets keyId when keyIdAlways is True" $+ mkAuthorityKeyIdentifier False True False False+ `shouldBe` AuthorityKeyIdentifier+ { akiKeyId = True+ , akiKeyIdAlways = True+ , akiIssuer = False+ , akiIssuerAlways = False+ }+ context "mkOID" $ do+ it "accepts a valid OID" $+ mkOID 1 [2, 3, 4] `shouldBe` Right (1 :| [2, 3, 4])+ it "rejects a negative first arc" $+ mkOID (-1) [] `shouldBe` Left InvalidFirstArc+ it "rejects a first arc greater than 2" $+ mkOID 3 [] `shouldBe` Left InvalidFirstArc+ it "rejects a negative subsequent arc" $+ mkOID 1 [-1] `shouldBe` Left NegativeArc+++notCA :: BasicConstraints+notCA = BasicConstraints{bcIsCA = False, bcPathLength = Nothing}+++isCA :: BasicConstraints+isCA = BasicConstraints{bcIsCA = True, bcPathLength = Nothing}+++caWithPathLen :: BasicConstraints+caWithPathLen = BasicConstraints{bcIsCA = True, bcPathLength = Just 3}+++simpleKeyUsage :: KeyUsage+simpleKeyUsage = fromList $ DigitalSignature :| [CRLSign]+++singleKeyUsage :: KeyUsage+singleKeyUsage = fromList $ DigitalSignature :| []+++allKeyUsageBits :: KeyUsage+allKeyUsageBits = fromList $ DigitalSignature :| [NonRepudiation, KeyEncipherment, DataEncipherment, KeyAgreement, KeyCertSign, CRLSign, EncipherOnly, DecipherOnly]+++simpleExtKeyUsage :: ExtKeyUsage+simpleExtKeyUsage = fromList $ ServerAuth :| [CodeSigning]+++singleExtKeyUsage :: ExtKeyUsage+singleExtKeyUsage = fromList $ ServerAuth :| []+++allExtKeyUsagePurposes :: ExtKeyUsage+allExtKeyUsagePurposes = fromList $ ServerAuth :| [ClientAuth, CodeSigning, EmailProtection, TimeStamping, OCSPSigning, AnyExtendedKeyUsage]+++simpleAKI :: AuthorityKeyIdentifier+simpleAKI = AuthorityKeyIdentifier{akiKeyId = True, akiKeyIdAlways = True, akiIssuer = False, akiIssuerAlways = False}+++keyIdOnly :: AuthorityKeyIdentifier+keyIdOnly = AuthorityKeyIdentifier{akiKeyId = True, akiKeyIdAlways = False, akiIssuer = False, akiIssuerAlways = False}+++issuerOnly :: AuthorityKeyIdentifier+issuerOnly = AuthorityKeyIdentifier{akiKeyId = False, akiKeyIdAlways = False, akiIssuer = True, akiIssuerAlways = False}+++issuerAlwaysAKI :: AuthorityKeyIdentifier+issuerAlwaysAKI = AuthorityKeyIdentifier{akiKeyId = False, akiKeyIdAlways = False, akiIssuer = False, akiIssuerAlways = True}+++bothAlways :: AuthorityKeyIdentifier+bothAlways = AuthorityKeyIdentifier{akiKeyId = False, akiKeyIdAlways = True, akiIssuer = False, akiIssuerAlways = True}+++allDisabled :: AuthorityKeyIdentifier+allDisabled = AuthorityKeyIdentifier{akiKeyId = False, akiKeyIdAlways = False, akiIssuer = False, akiIssuerAlways = False}+++simpleCP :: CertificatePolicies+simpleCP = mkCertificatePolicies (1 :| [2, 3, 4]) []+++twoOIDs :: CertificatePolicies+twoOIDs = mkCertificatePolicies (1 :| [2, 3, 4]) [2 :| [5, 4, 3]]
+ test/Spec.hs view
@@ -0,0 +1,46 @@+{- |+Module : Main+Copyright : (c) 2026 Tim Emiola+Maintainer : Tim Emiola <adetokunbo@emio.la>+SPDX-License-Identifier: BSD3++Test suite entry point for data-rfc5280.+-}+module Main where++import qualified Rfc5280.AssertSpec as Assert+import qualified Rfc5280.AuthorityInfoAccessSpec as AuthorityInfoAccess+import qualified Rfc5280.CRLDistributionPointsSpec as CRLDistributionPoints+import qualified Rfc5280.GeneralNameSpec as GeneralName+import qualified Rfc5280.HasOIDSpec as HasOID+import qualified Rfc5280.InhibitAnyPolicySpec as InhibitAnyPolicy+import qualified Rfc5280.IssuerAltNameSpec as IssuerAltName+import qualified Rfc5280.NameConstraintsSpec as NameConstraints+import qualified Rfc5280.PolicyMappingsSpec as PolicyMappings+import qualified Rfc5280.SubjectAltNameSpec as SubjectAltName+import qualified Rfc5280Spec as Extension+import System.IO+ ( BufferMode (..)+ , hSetBuffering+ , stderr+ , stdout+ )+import Test.Hspec+++main :: IO ()+main = do+ hSetBuffering stdout NoBuffering+ hSetBuffering stderr NoBuffering+ hspec $ do+ Assert.spec+ Extension.spec+ AuthorityInfoAccess.spec+ CRLDistributionPoints.spec+ GeneralName.spec+ HasOID.spec+ InhibitAnyPolicy.spec+ IssuerAltName.spec+ NameConstraints.spec+ PolicyMappings.spec+ SubjectAltName.spec