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
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