haskell-tls / hs-certificate

Certificate and Key Reader/Writer in haskell
60 stars 57 forks source link

Invalid assumptions about CertificateChain order #31

Closed mgomezch closed 10 years ago

mgomezch commented 10 years ago

At least the following code from Network.TLS.X509 assumes (correctly) that the first item in a certificate chain is the leaf certificate:

getCertificateChainLeaf :: CertificateChain -> SignedExact Certificate
getCertificateChainLeaf (CertificateChain [])    = error "empty certificate chain"
getCertificateChainLeaf (CertificateChain (x:_)) = x

One way to make a CertificateChain is this action in Network.TLS.Credentials:

-- | try to create a new credential object from a public certificate
-- and the associated private key that are stored on the filesystem
-- in PEM format.
credentialLoadX509 :: FilePath -- ^ public certificate (X.509 format)
                   -> FilePath -- ^ private key associated
                   -> IO (Either String Credential)
credentialLoadX509 certFile privateFile = do
    x509 <- readSignedObject certFile
    keys <- readKeyFile privateFile
    case keys of
        []    -> return $ Left "no keys found"
        (k:_) -> return $ Right (CertificateChain x509, k)

The function that actually reads the chain is defined thus in Data.X509.File:

-- | return all the signed objects in a file.
--
-- (only one type at a time).
readSignedObject :: (ASN1Object a, Eq a, Show a)
                 => FilePath
                 -> IO [X509.SignedExact a]
readSignedObject filepath = foldl pemToSigned [] <$> readPEMs filepath
  where pemToSigned acc pem =
            case X509.decodeSignedObject $ pemContent pem of
                Left _    -> acc
                Right obj -> obj : acc

readPEMs, from Data.PEM, returns a list whose items are ordered as they appear in the file. However, notice the list produced by readSignedObject is built with (:) and foldl, so it’ll be reversed.

I believe the problem should be fixed by using instead the following definition:

readSignedObject filepath = rights . fmap pemToSigned <$> readPEMs filepath
  where pemToSigned pem =                
            X509.decodeSignedObject $ pemContent pem

(with rights from Data.Either, of course)

I’d submit a pull request, but I don’t really know the first thing about TLS, PEM or these packages, and I have no idea how to test whether this solution makes sense. This just bit me from pointing Keter’s HTTPS listener setup at a certificate file that includes the chain that signed it, as it picks up one of the chain certificates as if it were the leaf, and Keter (actually warp-tls) fails at setting up connections since the key usage extension in the chain certificate does not specify it can encipher keys.

vincenthz commented 10 years ago

well, I'm not sure if there's any standard to certificate chain reading. The only time I actually had to read 2 certificates in a chain, it ended up correct through this function. I think the right thing to do is not to expect any order by default, and reconstruct the chain dynamically (as you can find which issuer signed which subject). At the same time it would be great to also provide a way to override the mechanism and allow both order.

Something like:

data CertificateChainOrder =
         LeafToRoot  -- ^ first certificate in the file is the leaf
       | RootToLeaf  -- ^ first certificate in the file is the root
       | ReconstructOrder -- ^ dynamically reconstruct the chain order.
vincenthz commented 10 years ago

closing for lack of further comment or actions.