Search code examples
haskellopenssh

How do I verify an OpenSSH signature in haskell?


I'm trying to verify an ed25519 signature in haskell.

For this I create a signature:

$ ssh-keygen -Y sign -f data/testkey_ed25519 -n file data/message.txt

I additionally create an allowed_signers file, since this is required for verification:

# allowed_signers
testprincipal ssh-ed25519 <public key> <comment>

ssh-keygen can then verify the signature:

$ ssh-keygen -Y verify -n file -s data/message.txt.sig -f data/authorized_signers -I testprincipal < data/message.txt
Good "file" signature for testprincipal with ED25519 key SHA256:RmoAVYLzWd7b2pTB0O1ovGu/KhXosg0zk++pJgIvQjw

I would like this functionality of ssh-keygen implemented in haskell (without external calls), but I am unable to reproduce the result using crypton:

{-# LANGUAGE OverloadedStrings #-}

module Main where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
import qualified Extra
import qualified Crypto.PubKey.Ed25519 as Ed25519
import Crypto.Error
import qualified Data.ByteString.Base64 as Base64


unarmorSignature :: C.ByteString -> C.ByteString
unarmorSignature = removeHeader . removeFooter . removeLinebreaks
        where
                header = "-----BEGIN SSH SIGNATURE-----"
                footer = "-----END SSH SIGNATURE-----"
                removeHeader = C.drop (C.length header) . snd . C.breakSubstring header
                removeFooter = fst . C.breakSubstring footer
                removeLinebreaks = C.filter (/= '\n')

main :: IO ()
main = do
        message <- BS.readFile "data/message.txt"
        print message

        signatureWrapped <- BS.readFile "data/message.txt.sig"
        let signatureDecoded = Extra.fromRight' $ Base64.decode $ unarmorSignature signatureWrapped
        let signatureBS = BS.takeEnd 64 signatureDecoded
        signature <- throwCryptoErrorIO $ Ed25519.signature signatureBS
        print signature

        keyfileContent <- BS.readFile "data/authorized_signers"
        let _:_:key:_ = C.words keyfileContent
        let keyDecoded = Extra.fromRight' $ Base64.decode key
        let Just keyBS = C.stripPrefix "\NUL\NUL\NUL\vssh-ed25519\NUL\NUL\NUL " keyDecoded
        key <- throwCryptoErrorIO $ Ed25519.publicKey keyBS
        print key

        let result = Ed25519.verify key message signature
        print result

The code is supposed to (very roughly) parse the signature and allowed_signers file formats (as per RFC8709) and extract the pure ed25519 key and signature from them. Then it's supposed to use them to verify that the signature was created using the key and message.

The program should print "true" at the end instead of "false".

A fully reproducible example can be found here


Solution

  • Unfortunately, in order to verify the signature, the signed data (test\n string) cannot be passed as-is.

    According to the protocol it should be in the following format:

    byte[6]   MAGIC_PREAMBLE
    string    namespace
    string    reserved
    string    hash_algorithm
    string    H(message)
    

    message is the test\n string here, but it also should be hashed and prepended by the additional metadata.

    The draft code that actually prints True as a result:

    ...
    import qualified Data.ByteString.Base16 as Base16 -- imported base16-bytestring
    import Crypto.Hash
    
    ...
    
    sha512 :: C.ByteString -> String
    sha512 bs = show (hash bs :: Digest SHA512)
    
    intArrayToByteString :: [Int] -> C.ByteString
    intArrayToByteString = BS.pack . map fromIntegral
    
    hexStringToIntList :: String -> Either String [Int]
    hexStringToIntList hexStr = do
        decoded <- Base16.decode (C.pack hexStr)
        return $ map fromIntegral (BS.unpack decoded)
    
    ...
    
    let hash = Extra.fromRight' $ hexStringToIntList $ sha512 message
    
    let intSignedText = [ 83, 83, 72, 83, 73, 71 -- SSHSIG magic header
                        , 0, 0, 0, 4, 102, 105, 108, 101 -- namespace ("file")
                        , 0, 0, 0, 0 -- reserved
                        , 0, 0, 0, 6, 115, 104, 97, 53, 49, 50  -- hash alg (sha512)
                        , 0, 0, 0, 64 ] ++ hash
    let result = Ed25519.verify key (intArrayToByteString intSignedText) signature
    print result